From 47efe7bff1842000250b13ca28f78a8b3f55271c Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 10:21:37 +0200 Subject: [PATCH 01/35] Add ordinary and reified GCD propagators Zdev-Change-Id: Z23e8ed204a2bc1ed69de169d26b3e327f2aa57728385b2abebae37b6df77b362 --- .zd/gcd/TASKS.md | 15 + .zd/gcd/area.toml | 6 + .zd/gcd/brief.md | 104 +++++++ ...ment-the-ternary-integer-gcd-constraint.md | 46 +++ ...-the-reified-integer-divides-constraint.md | 33 +++ ...y-and-reified-n-ary-product-constraints.md | 33 +++ ...d-reified-fixed-modulus-n-ary-product-c.md | 34 +++ Makefile.in | 2 +- gecode/int.hh | 21 ++ gecode/int/arithmetic.cpp | 29 ++ gecode/int/arithmetic.hh | 59 +++- gecode/int/arithmetic/gcd.hpp | 278 ++++++++++++++++++ test/int/arithmetic.cpp | 107 +++++++ 13 files changed, 765 insertions(+), 2 deletions(-) create mode 100644 .zd/gcd/TASKS.md create mode 100644 .zd/gcd/area.toml create mode 100644 .zd/gcd/brief.md create mode 100644 .zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md create mode 100644 .zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md create mode 100644 .zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md create mode 100644 .zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md create mode 100644 gecode/int/arithmetic/gcd.hpp diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md new file mode 100644 index 0000000000..7833a5560f --- /dev/null +++ b/.zd/gcd/TASKS.md @@ -0,0 +1,15 @@ + + +# Tasks: gcd + +- Total: 4 +- Ready: 3 +- Blocked: 0 +- Done: 1 + +| ID | Task | State | Blocked by | +| --- | --- | --- | --- | +| [gcd-001](tasks/001-implement-the-ternary-integer-gcd-constraint.md) | Implement ternary and reified integer GCD constraints | done | — | +| [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | ready | — | +| [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | ready | — | +| [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | ready | — | diff --git a/.zd/gcd/area.toml b/.zd/gcd/area.toml new file mode 100644 index 0000000000..6e68b0d627 --- /dev/null +++ b/.zd/gcd/area.toml @@ -0,0 +1,6 @@ +schema_version = 1 +tag = "gcd" +title = "Integer number-theoretic and product propagators" +objective = "Expose ordinary and reified integer constraints for GCD, divisibility, n-ary product, and fixed-modulus n-ary product, with sound propagation, public APIs, and focused regression coverage." +branch = "feature/gcd" +base_commit = "e1ec3da365f6c927d937b5ae9266d892a1cb9807" diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md new file mode 100644 index 0000000000..1eafced144 --- /dev/null +++ b/.zd/gcd/brief.md @@ -0,0 +1,104 @@ +# Integer number-theoretic and product propagators + +## Objective + +Expose ordinary and reified integer constraints for GCD, divisibility, n-ary +product, and fixed-modulus n-ary product, with sound propagation, public APIs, +and focused regression coverage. + +## Success + +- The public integer API can post `gcd(home, x, y, z, ipl)` for integer + variables and constrains `z` to the mathematical greatest common divisor of + `x` and `y`. +- The GCD relation also has a `Reify` overload supporting `RM_EQV`, `RM_IMP`, + and `RM_PMI` for the proposition `z = gcd(x,y)`. +- The public integer API can post + `divides(home, divisor, dividend, reify, ipl)` and supports `RM_EQV`, + `RM_IMP`, and `RM_PMI` with the same meaning as existing reified integer + constraints. +- The public integer API can post ordinary and reified + `product(home, factors, result, ..., ipl)` constraints for an n-ary exact + product. +- The public integer API can post ordinary and reified + `product_mod(home, factors, modulus, result, ..., ipl)` constraints for a + fixed positive integer modulus and canonical Euclidean residue. +- All constraints propagate soundly across negative, zero, sparse, assigned, + empty, repeated, and aliased variable domains as applicable; their actors + clone, subscribe, reschedule, rewrite, and subsume according to existing + Gecode conventions. +- Focused integer tests validate solutions and propagation through the existing + test harness. + +## Boundaries + +- Implement integer-variable propagators and their public posting APIs. +- Keep the work in the integer arithmetic constraint family and follow its + existing header/source/build organization. +- Do not add MiniModel expressions, FlatZinc builtins, or unrelated arithmetic + constraints. +- Do not introduce a new test harness or broad solver-wide test matrix. +- Only the reified `divides` API is in scope; a separate non-reified overload is + not required. +- `product_mod` accepts a fixed integer modulus only; an `IntVar` modulus + overload is outside scope. + +## Terms + +- `gcd(x,y)` is the unique nonnegative greatest common divisor. Negative + operands are interpreted by absolute value, `gcd(0,n)=abs(n)`, and + `gcd(0,0)=0`. +- `divisor divides dividend` means there exists an integer `k` such that + `dividend = divisor*k`. Consequently, zero divides zero, while zero does not + divide a nonzero integer. +- `product(x)` is the exact mathematical product of all variables in `x`, with + the empty product equal to one. Implementations must not silently overflow + while computing supports or bounds. +- `product_mod(x,m)` is the unique residue `r` such that + `r` is congruent to `product(x)` modulo the fixed positive integer `m` and + `0 <= r < m`. Thus `product_mod([],m) = 1 mod m`, including zero when `m=1`. +- Reification follows Gecode's `Reify` modes: equivalence (`RM_EQV`), forward + implication (`RM_IMP`), and reverse implication (`RM_PMI`). + +## Decisions + +- Use the mathematical zero semantics selected by the user rather than treating + a zero divisor as unconditionally invalid. +- Expose `IntPropLevel` consistently with neighboring integer arithmetic APIs; + implementations may map unsupported strengths to the strongest sound level + they actually provide, as existing APIs do. +- Prefer dedicated propagators and standard Gecode view/actor base classes over + a decomposition into multiplication, modulo, or extensional constraints. +- Use SMT-LIB's Euclidean remainder convention for `product_mod`, specialized + to a fixed positive modulus, rather than Gecode's existing dividend-signed + `mod` convention. +- Pair each ordinary n-ary product relation with a reified overload; reified + GCD means reifying the full proposition `z = gcd(x,y)`. + +## Open questions + +None. + +## Testing + +Use focused coverage in the existing `test/int/arithmetic.cpp` framework. Add +solution and propagation tests for small dense and sparse domains containing +negative values and zero, assigned inputs, variable aliasing, and cloning. For +`divides`, exercise all three reification modes and fixed/unfixed Boolean +controls, including the selected `0 | 0` behavior. For `gcd`, cover sign +normalization, the `(0,0)` case, and all reification modes. For `product`, cover +empty and singleton arrays, zeros, signs, repeated or aliased factors, result +aliasing, exact-product overflow boundaries, and all reification modes. For +`product_mod`, cover modulus one, negative factors, canonical residues, empty +and singleton arrays, aliasing, and all reification modes. Reuse existing +arithmetic test helpers and commands; do not create new test infrastructure or +an exhaustive large-domain performance matrix. + +## Validation + +- Build the affected integer library and test target with the repository's + established build configuration. +- Run the focused integer arithmetic tests. +- Run the broader integer test target if the established local build exposes it + without requiring a new configuration. +- Run `zd check gcd --format json` after task-state changes. diff --git a/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md b/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md new file mode 100644 index 0000000000..2789560599 --- /dev/null +++ b/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md @@ -0,0 +1,46 @@ ++++ +schema_version = 1 +id = "gcd-001" +key = "ternary-gcd" +area = "gcd" +status = "done" +blocked_by = [] ++++ +# Implement ternary and reified integer GCD constraints + +## Outcome + +Users can post ordinary and reified ternary integer constraints for z = gcd(x, y) with the mathematical sign and zero semantics recorded in the brief. + +## Boundaries + +- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. +- Do not implement the reified divides constraint in this task. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [x] Documented public gcd posting APIs for the ordinary relation and its Reify overload are available from gecode/int.hh and dispatch through the integer arithmetic implementation. +- [x] A dedicated ternary propagator implements sound posting, propagation, cloning, subscription, rescheduling, and subsumption for negative, zero, sparse, assigned, and aliased domains. +- [x] The reified GCD relation implements sound Boolean control, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI semantics for the full proposition z = gcd(x,y). +- [x] The result is constrained to the unique nonnegative gcd, including gcd(0,0)=0 and gcd(0,n)=abs(n). +- [x] Focused arithmetic tests cover ordinary and reified solution semantics, all reification modes, fixed and unfixed controls, propagation, sign normalization, zero cases, sparse domains, aliasing, and cloning without introducing new test infrastructure. +- [x] The affected integer library and focused tests build successfully. + +## Validation + +- Build the configured integer library and test executable. +- Run the focused ordinary and reified Int::Arithmetic::Gcd tests. +- Run the broader integer test target when available in the existing local build. + +## Result + +Implemented and independently verified ordinary and reified ternary integer GCD constraints with mathematical sign and zero semantics, alias-aware support filtering, and sound large-domain fallback. + +Validation: + +- Built gecodeint_shared and gecode-test successfully. +- Focused Arithmetic::Gcd tests passed all 15 variants. +- Broader Int::Arithmetic tests passed. +- Traditional header-list and staged CMake install checks included arithmetic/gcd.hpp. +- git diff --check passed. diff --git a/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md b/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md new file mode 100644 index 0000000000..5a2379c1d2 --- /dev/null +++ b/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md @@ -0,0 +1,33 @@ ++++ +schema_version = 1 +id = "gcd-002" +key = "reified-divides" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Implement the reified integer divides constraint + +## Outcome + +Users can reify the relation divisor divides dividend between two integer variables in every Gecode reification mode, using the mathematical zero semantics recorded in the brief. + +## Boundaries + +- Expose only the reified divides API requested in the brief; do not add a separate non-reified overload. +- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. +- Follow the brief's focused testing level and existing reified integer-test patterns. + +## Done when + +- [ ] The documented public divides posting API accepts two IntVar operands, Reify, and IntPropLevel and is available from gecode/int.hh. +- [ ] A dedicated reified propagator implements sound posting, propagation, cloning, subscription, rescheduling, rewriting or subsumption as appropriate, and Boolean control for RM_EQV, RM_IMP, and RM_PMI. +- [ ] The relation uses existential integer-multiplier semantics, including true for 0 divides 0 and false for 0 dividing any nonzero value. +- [ ] Focused arithmetic tests cover all reification modes, fixed and unfixed controls, true and false relations, negative and zero values, sparse domains, aliasing, and cloning without introducing new test infrastructure. +- [ ] The affected integer library and focused tests build successfully. + +## Validation + +- Build the configured integer library and test executable. +- Run the focused Int::Arithmetic::Divides tests. +- Run the broader integer test target when available in the existing local build. diff --git a/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md b/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md new file mode 100644 index 0000000000..73bbda280d --- /dev/null +++ b/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md @@ -0,0 +1,33 @@ ++++ +schema_version = 1 +id = "gcd-003" +key = "nary-product" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Implement ordinary and reified n-ary product constraints + +## Outcome + +Users can constrain an integer result to the exact product of an IntVarArgs array, ordinarily or through any Gecode reification mode. + +## Boundaries + +- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. +- Implement exact product only; fixed-modulus product belongs to its own task. +- Follow the brief's focused testing level and existing arithmetic and reification test patterns. + +## Done when + +- [ ] Documented public ordinary and Reify product posting APIs accept IntVarArgs factors, an IntVar result, and IntPropLevel and are available from gecode/int.hh. +- [ ] Dedicated n-ary actors implement sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. +- [ ] The relation uses exact mathematical multiplication, defines the empty product as one, handles singleton, repeated, zero, signed, sparse, assigned, and aliased variables, and never silently overflows internal arithmetic. +- [ ] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, empty and singleton arrays, zeros, signs, sparse domains, repetition, result aliasing, cloning, and representable-limit cases without new test infrastructure. +- [ ] The affected integer library and focused tests build successfully. + +## Validation + +- Build the configured integer library and test executable. +- Run the focused ordinary and reified Int::Arithmetic::Product tests. +- Run the broader integer test target when available in the existing local build. diff --git a/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md b/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md new file mode 100644 index 0000000000..afa16596fb --- /dev/null +++ b/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md @@ -0,0 +1,34 @@ ++++ +schema_version = 1 +id = "gcd-004" +key = "nary-product-mod" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Implement ordinary and reified fixed-modulus n-ary product constraints + +## Outcome + +Users can constrain an integer result to the canonical Euclidean residue of an IntVarArgs product under a fixed positive modulus, ordinarily or through any Gecode reification mode. + +## Boundaries + +- Accept a fixed positive int modulus only; do not add an IntVar modulus overload. +- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. +- Follow the brief's focused testing level and existing arithmetic and reification test patterns. + +## Done when + +- [ ] Documented public ordinary and Reify product_mod posting APIs accept IntVarArgs factors, a fixed int modulus, an IntVar result, and IntPropLevel and are available from gecode/int.hh. +- [ ] Posting rejects a nonpositive modulus through the repository's established integer-argument exception convention and constrains every result to the canonical range zero through modulus minus one. +- [ ] Dedicated n-ary actors implement overflow-safe modular arithmetic, sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. +- [ ] The relation uses SMT-LIB-compatible Euclidean residues, defines the empty product as one modulo the modulus, and handles modulus one, singleton, repeated, zero, signed, sparse, assigned, and aliased variables. +- [ ] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, modulus one, canonical residues for negative factors, empty and singleton arrays, zeros, sparse domains, repetition, result aliasing, and cloning without new test infrastructure. +- [ ] The affected integer library and focused tests build successfully. + +## Validation + +- Build the configured integer library and test executable. +- Run the focused ordinary and reified Int::Arithmetic::ProductMod tests. +- Run the broader integer test target when available in the existing local build. diff --git a/Makefile.in b/Makefile.in index b446405125..28ed044bb8 100755 --- a/Makefile.in +++ b/Makefile.in @@ -352,7 +352,7 @@ INTHDR0 = \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ arithmetic/abs.hpp arithmetic/max.hpp arithmetic/argmax.hpp \ - arithmetic/mult.hpp arithmetic/divmod.hpp \ + arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divmod.hpp \ arithmetic/pow-ops.hpp arithmetic/pow.hpp arithmetic/nroot.hpp \ bool/or.hpp bool/eq.hpp bool/lq.hpp bool/eqv.hpp bool/base.hpp \ bool/clause.hpp bool/ite.hpp \ diff --git a/gecode/int.hh b/gecode/int.hh index 2b6c2ea796..c9694169d2 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3031,6 +3031,27 @@ namespace Gecode { mult(Home home, IntVar x0, IntVar x1, IntVar x2, IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$\gcd(x_0,x_1)=x_2\f$ + * + * The greatest common divisor is nonnegative, with + * \f$\gcd(0,0)=0\f$. Negative operands are interpreted by absolute + * value. Supports domain consistency when domains are sufficiently small + * and sound bounds propagation otherwise. + */ + GECODE_INT_EXPORT void + gcd(Home home, IntVar x0, IntVar x1, IntVar x2, + IntPropLevel ipl=IPL_DEF); + + /** \brief Post propagator for + * \f$(\gcd(x_0,x_1)=x_2)\leftrightarrow r\f$ + * + * Supports all reification modes. The greatest common divisor is + * nonnegative, with \f$\gcd(0,0)=0\f$. + */ + GECODE_INT_EXPORT void + gcd(Home home, IntVar x0, IntVar x1, IntVar x2, Reify r, + IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$x_0\ \mathrm{div}\ x_1=x_2 \land x_0\ \mathrm{mod}\ x_1 = x_3\f$ * * Supports bounds consistency (\a ipl = IPL_BND, default). diff --git a/gecode/int/arithmetic.cpp b/gecode/int/arithmetic.cpp index e5650c9b5c..83e798636b 100755 --- a/gecode/int/arithmetic.cpp +++ b/gecode/int/arithmetic.cpp @@ -322,6 +322,35 @@ namespace Gecode { } } + void + gcd(Home home, IntVar x0, IntVar x1, IntVar x2, IntPropLevel) { + using namespace Int; + GECODE_POST; + GECODE_ES_FAIL(Arithmetic::Gcd::post(home,x0,x1,x2)); + } + + void + gcd(Home home, IntVar x0, IntVar x1, IntVar x2, Reify r, + IntPropLevel) { + using namespace Int; + GECODE_POST; + switch (r.mode()) { + case RM_EQV: + GECODE_ES_FAIL((Arithmetic::ReGcd + ::post(home,x0,x1,x2,r.var()))); + break; + case RM_IMP: + GECODE_ES_FAIL((Arithmetic::ReGcd + ::post(home,x0,x1,x2,r.var()))); + break; + case RM_PMI: + GECODE_ES_FAIL((Arithmetic::ReGcd + ::post(home,x0,x1,x2,r.var()))); + break; + default: GECODE_NEVER; + } + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 0f24c953bd..abe38b72b7 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -766,6 +766,64 @@ namespace Gecode { namespace Int { namespace Arithmetic { #include +namespace Gecode { namespace Int { namespace Arithmetic { + + /** + * \brief Domain propagator for \f$\gcd(x_0,x_1)=x_2\f$ + * + * Requires \code #include \endcode + * \ingroup FuncIntProp + */ + class Gcd : public TernaryPropagator { + protected: + using TernaryPropagator::x0; + using TernaryPropagator::x1; + using TernaryPropagator::x2; + /// Constructor for cloning \a p + Gcd(Space& home, Gcd& p); + /// Constructor for posting + Gcd(Home home, IntView x0, IntView x1, IntView x2); + public: + /// Post propagator + static ExecStatus post(Home home, IntView x0, IntView x1, IntView x2); + /// Copy propagator during cloning + virtual Actor* copy(Space& home); + /// Cost function + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Perform propagation + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + }; + + /** \brief Reified domain propagator for \f$\gcd(x_0,x_1)=x_2\f$ */ + template + class ReGcd : public Propagator { + protected: + IntView x0, x1, x2; + BoolView b; + /// Constructor for posting + ReGcd(Home home, IntView x0, IntView x1, IntView x2, BoolView b); + /// Constructor for cloning \a p + ReGcd(Space& home, ReGcd& p); + public: + /// Post propagator + static ExecStatus post(Home home, IntView x0, IntView x1, IntView x2, + BoolView b); + /// Copy propagator during cloning + virtual Actor* copy(Space& home); + /// Cost function + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Reschedule propagator + virtual void reschedule(Space& home); + /// Perform propagation + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size + virtual size_t dispose(Space& home); + }; + +}}} + +#include + namespace Gecode { namespace Int { namespace Arithmetic { /** @@ -857,4 +915,3 @@ namespace Gecode { namespace Int { namespace Arithmetic { #endif // STATISTICS: int-prop - diff --git a/gecode/int/arithmetic/gcd.hpp b/gecode/int/arithmetic/gcd.hpp new file mode 100644 index 0000000000..b7de5c0c11 --- /dev/null +++ b/gecode/int/arithmetic/gcd.hpp @@ -0,0 +1,278 @@ +/* -*- 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 + +namespace Gecode { namespace Int { namespace Arithmetic { + + /// Largest Cartesian product enumerated for domain propagation. + const unsigned long long gcd_support_limit = 200000ULL; + + forceinline int + gcd_value(int a, int b) { + unsigned int x = static_cast((a < 0) ? -a : a); + unsigned int y = static_cast((b < 0) ? -b : b); + while (y != 0U) { + unsigned int t = x % y; + x = y; y = t; + } + return static_cast(x); + } + + forceinline int + gcd_max_abs(const IntView& x) { + return std::max((x.min() < 0) ? -x.min() : x.min(), + (x.max() < 0) ? -x.max() : x.max()); + } + + forceinline bool + gcd_alias_ok(const IntView& x0, int a, const IntView& x1, int b, + const IntView& x2, int c) { + return ((x0 != x1) || (a == b)) && + ((x0 != x2) || (a == c)) && + ((x1 != x2) || (b == c)); + } + + forceinline bool + gcd_enumerable(const IntView& x0, const IntView& x1, const IntView& x2) { + unsigned long long n = x0.size(); + n *= x1.size(); + if (n > gcd_support_limit) + return false; + n *= x2.size(); + return n <= gcd_support_limit; + } + + /// Determine whether satisfying and violating assignments exist. + inline void + gcd_status(const IntView& x0, const IntView& x1, const IntView& x2, + bool& has_true, bool& has_false) { + has_true = has_false = false; + for (ViewValues i0(x0); i0(); ++i0) + for (ViewValues i1(x1); i1(); ++i1) + for (ViewValues i2(x2); i2(); ++i2) { + int a=i0.val(), b=i1.val(), c=i2.val(); + if (!gcd_alias_ok(x0,a,x1,b,x2,c)) + continue; + if (gcd_value(a,b) == c) + has_true = true; + else + has_false = true; + if (has_true && has_false) + return; + } + } + + /// Keep precisely the values supported by either the relation or its negation. + inline ExecStatus + gcd_filter(Space& home, IntView x0, IntView x1, IntView x2, bool positive) { + Region r; + unsigned int cap = static_cast( + static_cast(x0.size()) * x1.size() * x2.size()); + int* s0 = r.alloc(cap); + int* s1 = r.alloc(cap); + int* s2 = r.alloc(cap); + unsigned int n = 0; + for (ViewValues i0(x0); i0(); ++i0) + for (ViewValues i1(x1); i1(); ++i1) + for (ViewValues i2(x2); i2(); ++i2) { + int a=i0.val(), b=i1.val(), c=i2.val(); + if (!gcd_alias_ok(x0,a,x1,b,x2,c)) + continue; + if ((gcd_value(a,b) == c) == positive) { + s0[n]=a; s1[n]=b; s2[n]=c; n++; + } + } + if (n == 0) + return ES_FAILED; + std::sort(s0,s0+n); std::sort(s1,s1+n); std::sort(s2,s2+n); + unsigned int n0=1, n1=1, n2=1; + for (unsigned int i=1; i(home,y0,y1,y2) {} + + inline ExecStatus + Gcd::post(Home home, IntView x0, IntView x1, IntView x2) { + GECODE_ME_CHECK(x2.gq(home,0)); + GECODE_ME_CHECK(x2.lq(home,std::max(gcd_max_abs(x0),gcd_max_abs(x1)))); + if (x0.assigned() && x1.assigned()) { + GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); + return ES_OK; + } + (void) new (home) Gcd(home,x0,x1,x2); + return ES_OK; + } + + forceinline + Gcd::Gcd(Space& home, Gcd& p) + : TernaryPropagator(home,p) {} + + forceinline Actor* + Gcd::copy(Space& home) { + return new (home) Gcd(home,*this); + } + + forceinline PropCost + Gcd::cost(const Space&, const ModEventDelta&) const { + return PropCost::ternary(PropCost::HI); + } + + inline ExecStatus + Gcd::propagate(Space& home, const ModEventDelta&) { + GECODE_ME_CHECK(x2.gq(home,0)); + GECODE_ME_CHECK(x2.lq(home,std::max(gcd_max_abs(x0),gcd_max_abs(x1)))); + if (x0.assigned() && x1.assigned()) { + GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); + return home.ES_SUBSUMED(*this); + } + if (gcd_enumerable(x0,x1,x2)) + GECODE_ES_CHECK(gcd_filter(home,x0,x1,x2,true)); + if (x0.assigned() && x1.assigned() && x2.assigned()) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + + template + forceinline + ReGcd::ReGcd(Home home, IntView y0, IntView y1, IntView y2, BoolView b0) + : Propagator(home), x0(y0), x1(y1), x2(y2), b(b0) { + x0.subscribe(home,*this,PC_INT_DOM); + x1.subscribe(home,*this,PC_INT_DOM); + x2.subscribe(home,*this,PC_INT_DOM); + b.subscribe(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReGcd::post(Home home, IntView x0, IntView x1, IntView x2, BoolView b) { + if (b.one()) { + if (rm == RM_PMI) + return ES_OK; + return Gcd::post(home,x0,x1,x2); + } + if (b.zero() && (rm == RM_IMP)) + return ES_OK; + (void) new (home) ReGcd(home,x0,x1,x2,b); + return ES_OK; + } + + template + forceinline + ReGcd::ReGcd(Space& home, ReGcd& p) + : Propagator(home,p) { + x0.update(home,p.x0); x1.update(home,p.x1); x2.update(home,p.x2); + b.update(home,p.b); + } + + template + forceinline Actor* + ReGcd::copy(Space& home) { + return new (home) ReGcd(home,*this); + } + + template + forceinline PropCost + ReGcd::cost(const Space&, const ModEventDelta&) const { + return PropCost::ternary(PropCost::HI); + } + + template + forceinline void + ReGcd::reschedule(Space& home) { + x0.reschedule(home,*this,PC_INT_DOM); + x1.reschedule(home,*this,PC_INT_DOM); + x2.reschedule(home,*this,PC_INT_DOM); + b.reschedule(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReGcd::propagate(Space& home, const ModEventDelta&) { + if (b.one()) { + if (rm == RM_PMI) + return home.ES_SUBSUMED(*this); + GECODE_REWRITE(*this,Gcd::post(home(*this),x0,x1,x2)); + } + if (b.zero()) { + if (rm == RM_IMP) + return home.ES_SUBSUMED(*this); + if (gcd_enumerable(x0,x1,x2)) + GECODE_ES_CHECK(gcd_filter(home,x0,x1,x2,false)); + if (x0.assigned() && x1.assigned() && x2.assigned()) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + + bool has_true, has_false; + if (gcd_enumerable(x0,x1,x2)) { + gcd_status(x0,x1,x2,has_true,has_false); + if (!has_true) { + if (rm != RM_PMI) + GECODE_ME_CHECK(b.zero(home)); + return home.ES_SUBSUMED(*this); + } + if (!has_false) { + if (rm != RM_IMP) + GECODE_ME_CHECK(b.one(home)); + return home.ES_SUBSUMED(*this); + } + } + return ES_FIX; + } + + template + forceinline size_t + ReGcd::dispose(Space& home) { + x0.cancel(home,*this,PC_INT_DOM); + x1.cancel(home,*this,PC_INT_DOM); + x2.cancel(home,*this,PC_INT_DOM); + b.cancel(home,*this,PC_BOOL_VAL); + (void) Propagator::dispose(home); + return sizeof(*this); + } + +}}} diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 59ca93388e..97cdf588e4 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -48,6 +48,105 @@ namespace Test { namespace Int { * \ingroup TaskTestInt */ //@{ + /// Compute the mathematical greatest common divisor for testing. + int gcd_value(int a, int b) { + a = (a < 0) ? -a : a; + b = (b < 0) ? -b : b; + while (b != 0) { + int t = a % b; + a = b; b = t; + } + return a; + } + + /// %Test for the ternary greatest-common-divisor constraint + class GcdXYZ : public Test { + public: + /// Create and register test + GcdXYZ(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Gcd::XYZ::"+str(ipl)+"::"+s,3,d,true,ipl) {} + /// %Test whether \a x is solution + virtual bool solution(const Assignment& x) const { + return gcd_value(x[0],x[1]) == x[2]; + } + /// Post constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::gcd(home,x[0],x[1],x[2],ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::gcd(home,x[0],x[1],x[2],r,ipl); + } + }; + + /// %Test for gcd with identical operands + class GcdXXY : public Test { + public: + /// Create and register test + GcdXXY(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Gcd::XXY::"+str(ipl)+"::"+s,2,d,true,ipl) {} + /// %Test whether \a x is solution + virtual bool solution(const Assignment& x) const { + return gcd_value(x[0],x[0]) == x[1]; + } + /// Post constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::gcd(home,x[0],x[0],x[1],ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::gcd(home,x[0],x[0],x[1],r,ipl); + } + }; + + /// %Test for gcd with result aliased to the first operand + class GcdXYX : public Test { + public: + /// Create and register test + GcdXYX(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Gcd::XYX::"+str(ipl)+"::"+s,2,d,true,ipl) {} + /// %Test whether \a x is solution + virtual bool solution(const Assignment& x) const { + return gcd_value(x[0],x[1]) == x[0]; + } + /// Post constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::gcd(home,x[0],x[1],x[0],ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::gcd(home,x[0],x[1],x[0],r,ipl); + } + }; + + /// %Test for gcd with all variables aliased + class GcdXXX : public Test { + public: + /// Create and register test + GcdXXX(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Gcd::XXX::"+str(ipl)+"::"+s,1,d,true,ipl) {} + /// %Test whether \a x is solution + virtual bool solution(const Assignment& x) const { + return gcd_value(x[0],x[0]) == x[0]; + } + /// Post constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::gcd(home,x[0],x[0],x[0],ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::gcd(home,x[0],x[0],x[0],r,ipl); + } + }; + /// %Test for multiplication constraint class MultXYZ : public Test { public: @@ -1085,6 +1184,8 @@ namespace Test { namespace Int { Gecode::IntSet b(vb,9); Gecode::IntSet c(-8,8); Gecode::IntSet d(-70,70); + const int vg[7] = {-12,-6,-1,0,4,9,12}; + Gecode::IntSet g(vg,7); (void) new DivMod("A",a); (void) new DivMod("B",b); @@ -1100,6 +1201,12 @@ namespace Test { namespace Int { for (IntPropLevels ipls; ipls(); ++ipls) { + (void) new GcdXYZ("C",c,ipls.ipl()); + (void) new GcdXYZ("Sparse",g,ipls.ipl()); + (void) new GcdXXY("C",c,ipls.ipl()); + (void) new GcdXYX("C",c,ipls.ipl()); + (void) new GcdXXX("C",c,ipls.ipl()); + (void) new AbsXY("A",a,ipls.ipl()); (void) new AbsXY("B",b,ipls.ipl()); (void) new AbsXY("C",c,ipls.ipl()); From ac2bf84519a0c65c4c0158db8d7ba75ce55b29f1 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 10:34:59 +0200 Subject: [PATCH 02/35] Add reified divides propagator Zdev-Change-Id: Zc6a9a755441bc60bfbaa9f28638c9e80038509752e6de401cce089c8c70b1a01 --- .zd/gcd/TASKS.md | 6 +- ...-the-reified-integer-divides-constraint.md | 24 ++- Makefile.in | 2 +- gecode/int.hh | 10 + gecode/int/arithmetic.cpp | 22 ++ gecode/int/arithmetic.hh | 35 ++++ gecode/int/arithmetic/divides.hpp | 192 ++++++++++++++++++ test/int/arithmetic.cpp | 52 +++++ 8 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 gecode/int/arithmetic/divides.hpp diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 7833a5560f..9dc9112089 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,13 +3,13 @@ # Tasks: gcd - Total: 4 -- Ready: 3 +- Ready: 2 - Blocked: 0 -- Done: 1 +- Done: 2 | ID | Task | State | Blocked by | | --- | --- | --- | --- | | [gcd-001](tasks/001-implement-the-ternary-integer-gcd-constraint.md) | Implement ternary and reified integer GCD constraints | done | — | -| [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | ready | — | +| [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | done | — | | [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | ready | — | | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | ready | — | diff --git a/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md b/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md index 5a2379c1d2..b43dc13e72 100644 --- a/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md +++ b/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-002" key = "reified-divides" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Implement the reified integer divides constraint @@ -20,14 +20,26 @@ Users can reify the relation divisor divides dividend between two integer variab ## Done when -- [ ] The documented public divides posting API accepts two IntVar operands, Reify, and IntPropLevel and is available from gecode/int.hh. -- [ ] A dedicated reified propagator implements sound posting, propagation, cloning, subscription, rescheduling, rewriting or subsumption as appropriate, and Boolean control for RM_EQV, RM_IMP, and RM_PMI. -- [ ] The relation uses existential integer-multiplier semantics, including true for 0 divides 0 and false for 0 dividing any nonzero value. -- [ ] Focused arithmetic tests cover all reification modes, fixed and unfixed controls, true and false relations, negative and zero values, sparse domains, aliasing, and cloning without introducing new test infrastructure. -- [ ] The affected integer library and focused tests build successfully. +- [x] The documented public divides posting API accepts two IntVar operands, Reify, and IntPropLevel and is available from gecode/int.hh. +- [x] A dedicated reified propagator implements sound posting, propagation, cloning, subscription, rescheduling, rewriting or subsumption as appropriate, and Boolean control for RM_EQV, RM_IMP, and RM_PMI. +- [x] The relation uses existential integer-multiplier semantics, including true for 0 divides 0 and false for 0 dividing any nonzero value. +- [x] Focused arithmetic tests cover all reification modes, fixed and unfixed controls, true and false relations, negative and zero values, sparse domains, aliasing, and cloning without introducing new test infrastructure. +- [x] The affected integer library and focused tests build successfully. ## Validation - Build the configured integer library and test executable. - Run the focused Int::Arithmetic::Divides tests. - Run the broader integer test target when available in the existing local build. + +## Result + +Implemented and independently verified the reified integer divides relation for all reification modes with existential multiplier semantics, signed operands, zero semantics, aliases, and sound large-domain monitoring. + +Validation: + +- Built gecodeint_shared and gecode-test successfully. +- Focused Arithmetic::Divides tests passed all nine groups for five iterations. +- Broader Int::Arithmetic tests passed. +- Fresh CMake install included arithmetic/divides.hpp. +- git diff --check and zd check passed. diff --git a/Makefile.in b/Makefile.in index 28ed044bb8..2dc11f6250 100755 --- a/Makefile.in +++ b/Makefile.in @@ -352,7 +352,7 @@ INTHDR0 = \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ arithmetic/abs.hpp arithmetic/max.hpp arithmetic/argmax.hpp \ - arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divmod.hpp \ + arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/divmod.hpp \ arithmetic/pow-ops.hpp arithmetic/pow.hpp arithmetic/nroot.hpp \ bool/or.hpp bool/eq.hpp bool/lq.hpp bool/eqv.hpp bool/base.hpp \ bool/clause.hpp bool/ite.hpp \ diff --git a/gecode/int.hh b/gecode/int.hh index c9694169d2..f68d9d16f4 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3052,6 +3052,16 @@ namespace Gecode { gcd(Home home, IntVar x0, IntVar x1, IntVar x2, Reify r, IntPropLevel ipl=IPL_DEF); + /** \brief Reify whether \a divisor divides \a dividend + * + * Divisibility means that an integer \f$k\f$ exists such that + * \f$dividend=divisor\cdot k\f$. Consequently, zero divides zero, but + * zero does not divide a nonzero integer. Supports all reification modes. + */ + GECODE_INT_EXPORT void + divides(Home home, IntVar divisor, IntVar dividend, Reify r, + IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$x_0\ \mathrm{div}\ x_1=x_2 \land x_0\ \mathrm{mod}\ x_1 = x_3\f$ * * Supports bounds consistency (\a ipl = IPL_BND, default). diff --git a/gecode/int/arithmetic.cpp b/gecode/int/arithmetic.cpp index 83e798636b..03e89ed152 100755 --- a/gecode/int/arithmetic.cpp +++ b/gecode/int/arithmetic.cpp @@ -351,6 +351,28 @@ namespace Gecode { } } + void + divides(Home home, IntVar divisor, IntVar dividend, Reify r, + IntPropLevel) { + using namespace Int; + GECODE_POST; + switch (r.mode()) { + case RM_EQV: + GECODE_ES_FAIL((Arithmetic::ReDivides + ::post(home,divisor,dividend,r.var()))); + break; + case RM_IMP: + GECODE_ES_FAIL((Arithmetic::ReDivides + ::post(home,divisor,dividend,r.var()))); + break; + case RM_PMI: + GECODE_ES_FAIL((Arithmetic::ReDivides + ::post(home,divisor,dividend,r.var()))); + break; + default: GECODE_NEVER; + } + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index abe38b72b7..5a15e8e491 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -824,6 +824,41 @@ namespace Gecode { namespace Int { namespace Arithmetic { #include +namespace Gecode { namespace Int { namespace Arithmetic { + + /** + * \brief Reified domain propagator for divisibility + * + * The relation is \f$\exists k\in\mathbb Z:x_1=x_0k\f$. + * Requires \code #include \endcode + * \ingroup FuncIntProp + */ + template + class ReDivides : + public ReBinaryPropagator { + protected: + using ReBinaryPropagator::x0; + using ReBinaryPropagator::x1; + using ReBinaryPropagator::b; + /// Constructor for posting + ReDivides(Home home, IntView x0, IntView x1, BoolView b); + /// Constructor for cloning \a p + ReDivides(Space& home, ReDivides& p); + public: + /// Post propagator + static ExecStatus post(Home home, IntView x0, IntView x1, BoolView b); + /// Copy propagator during cloning + virtual Actor* copy(Space& home); + /// Cost function + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Perform propagation + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + }; + +}}} + +#include + namespace Gecode { namespace Int { namespace Arithmetic { /** diff --git a/gecode/int/arithmetic/divides.hpp b/gecode/int/arithmetic/divides.hpp new file mode 100644 index 0000000000..256cd3c368 --- /dev/null +++ b/gecode/int/arithmetic/divides.hpp @@ -0,0 +1,192 @@ +/* -*- 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 + +namespace Gecode { namespace Int { namespace Arithmetic { + + /// Largest Cartesian product enumerated for domain propagation. + const unsigned long long divides_support_limit = 200000ULL; + + forceinline bool + divides_value(int divisor, int dividend) { + return (divisor == 0) ? (dividend == 0) : (dividend % divisor == 0); + } + + forceinline bool + divides_enumerable(const IntView& x0, const IntView& x1) { + return (static_cast(x0.size()) * x1.size()) <= + divides_support_limit; + } + + inline RelTest + divides_status(const IntView& x0, const IntView& x1) { + if (x0 == x1) + return RT_TRUE; + if (x1.assigned() && (x1.val() == 0)) + return RT_TRUE; + if (x0.assigned() && ((x0.val() == 1) || (x0.val() == -1))) + return RT_TRUE; + if (x0.assigned() && (x0.val() == 0) && !x1.in(0)) + return RT_FALSE; + if (x0.assigned() && x1.assigned()) + return divides_value(x0.val(),x1.val()) ? RT_TRUE : RT_FALSE; + if (!divides_enumerable(x0,x1)) + return RT_MAYBE; + bool has_true = false, has_false = false; + for (ViewValues i0(x0); i0(); ++i0) + for (ViewValues i1(x1); i1(); ++i1) { + if (divides_value(i0.val(),i1.val())) + has_true = true; + else + has_false = true; + if (has_true && has_false) + return RT_MAYBE; + } + return has_true ? RT_TRUE : RT_FALSE; + } + + /// Keep precisely values with support in the relation (or its negation). + inline ExecStatus + divides_filter(Space& home, IntView x0, IntView x1, bool positive) { + if (x0 == x1) + return positive ? ES_OK : ES_FAILED; + if (!divides_enumerable(x0,x1)) + return ES_OK; + + Region r; + unsigned int cap = x0.size() * x1.size(); + int* s0 = r.alloc(cap); + int* s1 = r.alloc(cap); + unsigned int n = 0; + for (ViewValues i0(x0); i0(); ++i0) + for (ViewValues i1(x1); i1(); ++i1) + if (divides_value(i0.val(),i1.val()) == positive) { + s0[n] = i0.val(); s1[n] = i1.val(); n++; + } + if (n == 0) + return ES_FAILED; + std::sort(s0,s0+n); std::sort(s1,s1+n); + unsigned int n0=1, n1=1; + for (unsigned int i=1; i + forceinline + ReDivides::ReDivides(Home home, IntView y0, IntView y1, BoolView b0) + : ReBinaryPropagator(home,y0,y1,b0) {} + + template + inline ExecStatus + ReDivides::post(Home home, IntView x0, IntView x1, BoolView b) { + if (b.one() && (rm == RM_PMI)) + return ES_OK; + if (b.zero() && (rm == RM_IMP)) + return ES_OK; + RelTest rt = divides_status(x0,x1); + if (rt == RT_TRUE) { + if (rm != RM_IMP) + GECODE_ME_CHECK(b.one(home)); + return ES_OK; + } + if (rt == RT_FALSE) { + if (rm != RM_PMI) + GECODE_ME_CHECK(b.zero(home)); + return ES_OK; + } + (void) new (home) ReDivides(home,x0,x1,b); + return ES_OK; + } + + template + forceinline + ReDivides::ReDivides(Space& home, ReDivides& p) + : ReBinaryPropagator(home,p) {} + + template + forceinline Actor* + ReDivides::copy(Space& home) { + return new (home) ReDivides(home,*this); + } + + template + forceinline PropCost + ReDivides::cost(const Space&, const ModEventDelta&) const { + return PropCost::binary(PropCost::HI); + } + + template + inline ExecStatus + ReDivides::propagate(Space& home, const ModEventDelta&) { + if (b.one()) { + if (rm == RM_PMI) + return home.ES_SUBSUMED(*this); + GECODE_ES_CHECK(divides_filter(home,x0,x1,true)); + if (divides_status(x0,x1) == RT_TRUE) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + if (b.zero()) { + if (rm == RM_IMP) + return home.ES_SUBSUMED(*this); + GECODE_ES_CHECK(divides_filter(home,x0,x1,false)); + if (divides_status(x0,x1) == RT_FALSE) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + + switch (divides_status(x0,x1)) { + case RT_TRUE: + if (rm != RM_IMP) + GECODE_ME_CHECK(b.one_none(home)); + break; + case RT_FALSE: + if (rm != RM_PMI) + GECODE_ME_CHECK(b.zero_none(home)); + break; + case RT_MAYBE: + return ES_FIX; + default: GECODE_NEVER; + } + return home.ES_SUBSUMED(*this); + } + +}}} diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 97cdf588e4..24c0c6be40 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -147,6 +147,54 @@ namespace Test { namespace Int { } }; + /// %Test for the reified divisibility constraint + class DividesXY : public Test { + public: + /// Create and register test + DividesXY(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Divides::XY::"+str(ipl)+"::"+s, + 2,d,true,ipl) {} + /// %Test whether \a x is solution + virtual bool solution(const Assignment& x) const { + return (x[0] == 0) ? (x[1] == 0) : (x[1] % x[0] == 0); + } + /// The constraint is deliberately exposed only as reified + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::BoolVar b(home,1,1); + Gecode::divides(home,x[0],x[1],Gecode::Reify(b),ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::divides(home,x[0],x[1],r,ipl); + } + }; + + /// %Test divisibility with aliased operands + class DividesXX : public Test { + public: + /// Create and register test + DividesXX(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Divides::XX::"+str(ipl)+"::"+s, + 1,d,true,ipl) {} + /// Every integer divides itself, including zero + virtual bool solution(const Assignment&) const { + return true; + } + /// The constraint is deliberately exposed only as reified + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::BoolVar b(home,1,1); + Gecode::divides(home,x[0],x[0],Gecode::Reify(b),ipl); + } + /// Post reified constraint on \a x + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::divides(home,x[0],x[0],r,ipl); + } + }; + /// %Test for multiplication constraint class MultXYZ : public Test { public: @@ -1207,6 +1255,10 @@ namespace Test { namespace Int { (void) new GcdXYX("C",c,ipls.ipl()); (void) new GcdXXX("C",c,ipls.ipl()); + (void) new DividesXY("C",c,ipls.ipl()); + (void) new DividesXY("Sparse",g,ipls.ipl()); + (void) new DividesXX("C",c,ipls.ipl()); + (void) new AbsXY("A",a,ipls.ipl()); (void) new AbsXY("B",b,ipls.ipl()); (void) new AbsXY("C",c,ipls.ipl()); From e2302d22e8fef5b4b35f80bf9e49dc0d7a6ebd8d Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 10:56:29 +0200 Subject: [PATCH 03/35] Add n-ary product propagators Zdev-Change-Id: Z80743f0b0c047b65076611f28d9fffc994e0fa5302fe3aca0132eb0bb479b06c --- .zd/gcd/TASKS.md | 6 +- ...y-and-reified-n-ary-product-constraints.md | 24 +- Makefile.in | 2 +- gecode/int.hh | 18 ++ gecode/int/arithmetic.cpp | 31 ++ gecode/int/arithmetic.hh | 43 +++ gecode/int/arithmetic/product.hpp | 306 ++++++++++++++++++ test/int/arithmetic.cpp | 113 +++++++ 8 files changed, 533 insertions(+), 10 deletions(-) create mode 100644 gecode/int/arithmetic/product.hpp diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 9dc9112089..0b3f8134a5 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,13 +3,13 @@ # Tasks: gcd - Total: 4 -- Ready: 2 +- Ready: 1 - Blocked: 0 -- Done: 2 +- Done: 3 | ID | Task | State | Blocked by | | --- | --- | --- | --- | | [gcd-001](tasks/001-implement-the-ternary-integer-gcd-constraint.md) | Implement ternary and reified integer GCD constraints | done | — | | [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | done | — | -| [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | ready | — | +| [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | ready | — | diff --git a/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md b/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md index 73bbda280d..5d1a853c2e 100644 --- a/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md +++ b/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-003" key = "nary-product" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Implement ordinary and reified n-ary product constraints @@ -20,14 +20,26 @@ Users can constrain an integer result to the exact product of an IntVarArgs arra ## Done when -- [ ] Documented public ordinary and Reify product posting APIs accept IntVarArgs factors, an IntVar result, and IntPropLevel and are available from gecode/int.hh. -- [ ] Dedicated n-ary actors implement sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. -- [ ] The relation uses exact mathematical multiplication, defines the empty product as one, handles singleton, repeated, zero, signed, sparse, assigned, and aliased variables, and never silently overflows internal arithmetic. -- [ ] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, empty and singleton arrays, zeros, signs, sparse domains, repetition, result aliasing, cloning, and representable-limit cases without new test infrastructure. -- [ ] The affected integer library and focused tests build successfully. +- [x] Documented public ordinary and Reify product posting APIs accept IntVarArgs factors, an IntVar result, and IntPropLevel and are available from gecode/int.hh. +- [x] Dedicated n-ary actors implement sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. +- [x] The relation uses exact mathematical multiplication, defines the empty product as one, handles singleton, repeated, zero, signed, sparse, assigned, and aliased variables, and never silently overflows internal arithmetic. +- [x] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, empty and singleton arrays, zeros, signs, sparse domains, repetition, result aliasing, cloning, and representable-limit cases without new test infrastructure. +- [x] The affected integer library and focused tests build successfully. ## Validation - Build the configured integer library and test executable. - Run the focused ordinary and reified Int::Arithmetic::Product tests. - Run the broader integer test target when available in the existing local build. + +## Result + +Implemented and independently verified ordinary and reified exact n-ary product constraints with empty-product semantics, alias-aware support filtering, checked arithmetic, and sound large-domain fallback. + +Validation: + +- Built gecodeint_shared and gecode-test successfully. +- All 18 focused Int::Arithmetic::Product cases passed. +- Broader Int::Arithmetic suite passed. +- Fresh install and installed-consumer smoke tests passed, including large-domain assignment and cloning. +- git diff --check and zd check passed. diff --git a/Makefile.in b/Makefile.in index 2dc11f6250..0a7e7353ac 100755 --- a/Makefile.in +++ b/Makefile.in @@ -352,7 +352,7 @@ INTHDR0 = \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ arithmetic/abs.hpp arithmetic/max.hpp arithmetic/argmax.hpp \ - arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/divmod.hpp \ + arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/product.hpp arithmetic/divmod.hpp \ arithmetic/pow-ops.hpp arithmetic/pow.hpp arithmetic/nroot.hpp \ bool/or.hpp bool/eq.hpp bool/lq.hpp bool/eqv.hpp bool/base.hpp \ bool/clause.hpp bool/ite.hpp \ diff --git a/gecode/int.hh b/gecode/int.hh index f68d9d16f4..60b4215b14 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3062,6 +3062,24 @@ namespace Gecode { divides(Home home, IntVar divisor, IntVar dividend, Reify r, IntPropLevel ipl=IPL_DEF); + /** \brief Constrain \a y to the exact product of the variables in \a x + * + * The product of an empty array is one. + * \ingroup TaskModelInt + */ + GECODE_INT_EXPORT void + product(Home home, const IntVarArgs& x, IntVar y, + IntPropLevel ipl=IPL_DEF); + + /** \brief Reify whether \a y is the exact product of the variables in \a x + * + * The product of an empty array is one. + * \ingroup TaskModelInt + */ + GECODE_INT_EXPORT void + product(Home home, const IntVarArgs& x, IntVar y, Reify r, + IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$x_0\ \mathrm{div}\ x_1=x_2 \land x_0\ \mathrm{mod}\ x_1 = x_3\f$ * * Supports bounds consistency (\a ipl = IPL_BND, default). diff --git a/gecode/int/arithmetic.cpp b/gecode/int/arithmetic.cpp index 03e89ed152..99d6a226fc 100755 --- a/gecode/int/arithmetic.cpp +++ b/gecode/int/arithmetic.cpp @@ -373,6 +373,37 @@ namespace Gecode { } } + void + product(Home home, const IntVarArgs& x, IntVar y, IntPropLevel) { + using namespace Int; + GECODE_POST; + ViewArray xv(home,x); + GECODE_ES_FAIL(Arithmetic::Product::post(home,xv,y)); + } + + void + product(Home home, const IntVarArgs& x, IntVar y, Reify r, + IntPropLevel) { + using namespace Int; + GECODE_POST; + ViewArray xv(home,x); + switch (r.mode()) { + case RM_EQV: + GECODE_ES_FAIL((Arithmetic::ReProduct + ::post(home,xv,y,r.var()))); + break; + case RM_IMP: + GECODE_ES_FAIL((Arithmetic::ReProduct + ::post(home,xv,y,r.var()))); + break; + case RM_PMI: + GECODE_ES_FAIL((Arithmetic::ReProduct + ::post(home,xv,y,r.var()))); + break; + default: GECODE_NEVER; + } + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 5a15e8e491..c41b6e1479 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -859,6 +859,49 @@ namespace Gecode { namespace Int { namespace Arithmetic { #include +namespace Gecode { namespace Int { namespace Arithmetic { + + /** \brief Domain propagator for an exact n-ary product + * \ingroup FuncIntProp + */ + class Product : public NaryOnePropagator { + protected: + using NaryOnePropagator::x; + using NaryOnePropagator::y; + Product(Home home, ViewArray& x, IntView y); + Product(Space& home, Product& p); + public: + static ExecStatus post(Home home, ViewArray& x, IntView y); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + }; + + /** \brief Reified domain propagator for an exact n-ary product + * \ingroup FuncIntProp + */ + template + class ReProduct : public Propagator { + protected: + ViewArray x; + IntView y; + BoolView b; + ReProduct(Home home, ViewArray& x, IntView y, BoolView b); + ReProduct(Space& home, ReProduct& p); + public: + static ExecStatus post(Home home, ViewArray& x, IntView y, + BoolView b); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual void reschedule(Space& home); + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + virtual size_t dispose(Space& home); + }; + +}}} + +#include + namespace Gecode { namespace Int { namespace Arithmetic { /** diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp new file mode 100644 index 0000000000..e141237699 --- /dev/null +++ b/gecode/int/arithmetic/product.hpp @@ -0,0 +1,306 @@ +/* -*- 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 + +namespace Gecode { namespace Int { namespace Arithmetic { + + const unsigned long long product_support_limit = 200000ULL; + + /// Return whether the Cartesian product can be enumerated safely. + inline bool + product_enumerable(const ViewArray& x, const IntView& y) { + unsigned long long n = y.size(); + for (int i=0; i product_support_limit / s)) + return false; + n *= s; + } + return n <= product_support_limit; + } + + /// Evaluate a tuple without overflowing internal arithmetic. + inline bool + product_value(const int* v, int n, int& p) { + for (int i=0; i(v[i]))) + return false; + q *= static_cast(v[i]); + } + if (!Limits::valid(q)) + return false; + p = static_cast(q); + return true; + } + + /// Check that one enumerated tuple respects all aliased views. + inline bool + product_alias_ok(const ViewArray& x, const IntView& y, + const int* v) { + for (int i=0; i& x, IntView y, + bool positive, bool filter, + bool& has_true, bool& has_false) { + const int n = x.size(); + Region r; + unsigned int* size = r.alloc(n+1); + unsigned int* pos = r.alloc(n+1); + int** values = r.alloc(n+1); + unsigned long long cap64 = y.size(); + for (int i=0; i(cap64); + int** support = filter ? r.alloc(n+1) : nullptr; + + for (int i=0; i(size[i]); + unsigned int k=0; + for (ViewValues vi(x[i]); vi(); ++vi) + values[i][k++] = vi.val(); + if (filter) support[i] = r.alloc(cap); + } + size[n] = y.size(); pos[n] = 0; + values[n] = r.alloc(size[n]); + unsigned int k=0; + for (ViewValues vi(y); vi(); ++vi) + values[n][k++] = vi.val(); + if (filter) support[n] = r.alloc(cap); + + int* tuple = r.alloc(n+1); + unsigned int ns = 0; + has_true = has_false = false; + bool more = true; + while (more) { + for (int i=0; i<=n; i++) tuple[i] = values[i][pos[i]]; + if (product_alias_ok(x,y,tuple)) { + int p; + bool relation = product_value(tuple,n,p) && (p == tuple[n]); + has_true |= relation; has_false |= !relation; + if (filter && (relation == positive)) { + for (int i=0; i<=n; i++) support[i][ns] = tuple[i]; + ns++; + } + } + int i=n; + while ((i >= 0) && (++pos[i] == size[i])) { + pos[i]=0; i--; + } + more = i >= 0; + } + + if (!filter) + return ES_OK; + if (ns == 0) + return ES_FAILED; + for (int i=0; i<=n; i++) { + std::sort(support[i],support[i]+ns); + unsigned int nu=1; + for (unsigned int j=1; j& z, IntView w) + : NaryOnePropagator(home,z,w) {} + + inline ExecStatus + Product::post(Home home, ViewArray& x, IntView y) { + if (x.size() == 0) { + GECODE_ME_CHECK(y.eq(home,1)); + return ES_OK; + } + (void) new (home) Product(home,x,y); + return ES_OK; + } + + forceinline + Product::Product(Space& home, Product& p) + : NaryOnePropagator(home,p) {} + + forceinline Actor* + Product::copy(Space& home) { + return new (home) Product(home,*this); + } + + forceinline PropCost + Product::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+1); + } + + inline ExecStatus + Product::propagate(Space& home, const ModEventDelta&) { + if (product_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_supports(home,x,y,true,true,ht,hf)); + } + bool assigned = y.assigned(); + for (int i=0; assigned && (i + forceinline + ReProduct::ReProduct(Home home, ViewArray& z, IntView w, + BoolView c) + : Propagator(home), x(z), y(w), b(c) { + x.subscribe(home,*this,PC_INT_DOM); + y.subscribe(home,*this,PC_INT_DOM); + b.subscribe(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProduct::post(Home home, ViewArray& x, IntView y, BoolView b) { + if (b.one() && (rm == RM_PMI)) return ES_OK; + if (b.zero() && (rm == RM_IMP)) return ES_OK; + if (x.size() == 0) { + bool t = y.assigned() ? (y.val() == 1) : false; + if (b.one() && (rm != RM_PMI)) { + GECODE_ME_CHECK(y.eq(home,1)); return ES_OK; + } + if (b.zero() && (rm != RM_IMP)) { + GECODE_ME_CHECK(y.nq(home,1)); return ES_OK; + } + if (!y.in(1)) { + if (rm != RM_PMI) GECODE_ME_CHECK(b.zero(home)); + return ES_OK; + } + if (t) { + if (rm != RM_IMP) GECODE_ME_CHECK(b.one(home)); + return ES_OK; + } + } + (void) new (home) ReProduct(home,x,y,b); + return ES_OK; + } + + template + forceinline + ReProduct::ReProduct(Space& home, ReProduct& p) + : Propagator(home,p) { + x.update(home,p.x); y.update(home,p.y); b.update(home,p.b); + } + + template + forceinline Actor* + ReProduct::copy(Space& home) { + return new (home) ReProduct(home,*this); + } + + template + forceinline PropCost + ReProduct::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+2); + } + + template + forceinline void + ReProduct::reschedule(Space& home) { + x.reschedule(home,*this,PC_INT_DOM); + y.reschedule(home,*this,PC_INT_DOM); + b.reschedule(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProduct::propagate(Space& home, const ModEventDelta&) { + if (b.one()) { + if (rm == RM_PMI) return home.ES_SUBSUMED(*this); + GECODE_REWRITE(*this,Product::post(home(*this),x,y)); + } + if (b.zero()) { + if (rm == RM_IMP) return home.ES_SUBSUMED(*this); + if (product_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_supports(home,x,y,false,true,ht,hf)); + if (!ht) return home.ES_SUBSUMED(*this); + } + return ES_FIX; + } + if (product_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_supports(home,x,y,true,false,ht,hf)); + if (!ht) { + if (rm != RM_PMI) GECODE_ME_CHECK(b.zero_none(home)); + return home.ES_SUBSUMED(*this); + } + if (!hf) { + if (rm != RM_IMP) GECODE_ME_CHECK(b.one_none(home)); + return home.ES_SUBSUMED(*this); + } + } + return ES_FIX; + } + + template + forceinline size_t + ReProduct::dispose(Space& home) { + x.cancel(home,*this,PC_INT_DOM); + y.cancel(home,*this,PC_INT_DOM); + b.cancel(home,*this,PC_BOOL_VAL); + (void) Propagator::dispose(home); + return sizeof(*this); + } + +}}} diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 24c0c6be40..3dc3eba479 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -195,6 +195,107 @@ namespace Test { namespace Int { } }; + /// Evaluate an exact product for testing without overflowing. + bool product_value(const Assignment& x, int n, int& product) { + for (int i=0; i(x[i]))) + return false; + p *= static_cast(x[i]); + } + if (!Gecode::Int::Limits::valid(p)) + return false; + product = static_cast(p); + return true; + } + + /// %Test for an ordinary and reified three-factor product + class ProductXYZR : public Test { + public: + ProductXYZR(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Product::XYZR::"+str(ipl)+"::"+s, + 4,d,true,ipl) {} + virtual bool solution(const Assignment& x) const { + int p; + return product_value(x,3,p) && (p == x[3]); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::IntVarArgs f({x[0],x[1],x[2]}); + Gecode::product(home,f,x[3],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::IntVarArgs f({x[0],x[1],x[2]}); + Gecode::product(home,f,x[3],r,ipl); + } + }; + + /// %Test for the empty product + class ProductEmpty : public Test { + public: + ProductEmpty(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Product::Empty::"+str(ipl)+"::"+s, + 1,d,true,ipl) {} + virtual bool solution(const Assignment& x) const { + return x[0] == 1; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product(home,Gecode::IntVarArgs(),x[0],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product(home,Gecode::IntVarArgs(),x[0],r,ipl); + } + }; + + /// %Test for the singleton product + class ProductSingleton : public Test { + public: + ProductSingleton(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Product::Singleton::"+str(ipl)+"::"+s, + 2,d,true,ipl) {} + virtual bool solution(const Assignment& x) const { + return x[0] == x[1]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product(home,Gecode::IntVarArgs({x[0]}),x[1],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product(home,Gecode::IntVarArgs({x[0]}),x[1],r,ipl); + } + }; + + /// %Test repeated factors and result aliasing + class ProductXXYAlias : public Test { + public: + ProductXXYAlias(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::Product::XXYAlias::"+str(ipl)+"::"+s, + 2,d,true,ipl) {} + virtual bool solution(const Assignment& x) const { + long long int p = static_cast(x[0]) * x[0] * x[1]; + return (p >= Gecode::Int::Limits::min) && + (p <= Gecode::Int::Limits::max) && (p == x[1]); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product(home,Gecode::IntVarArgs({x[0],x[0],x[1]}),x[1],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product(home,Gecode::IntVarArgs({x[0],x[0],x[1]}),x[1],r,ipl); + } + }; + /// %Test for multiplication constraint class MultXYZ : public Test { public: @@ -1234,6 +1335,11 @@ namespace Test { namespace Int { Gecode::IntSet d(-70,70); const int vg[7] = {-12,-6,-1,0,4,9,12}; Gecode::IntSet g(vg,7); + const int vp[5] = { + Gecode::Int::Limits::min,-1,0,1,Gecode::Int::Limits::max + }; + Gecode::IntSet p(vp,5); + Gecode::IntSet q(-2,2); (void) new DivMod("A",a); (void) new DivMod("B",b); @@ -1259,6 +1365,13 @@ namespace Test { namespace Int { (void) new DividesXY("Sparse",g,ipls.ipl()); (void) new DividesXX("C",c,ipls.ipl()); + (void) new ProductXYZR("C",q,ipls.ipl()); + (void) new ProductXYZR("Sparse",g,ipls.ipl()); + (void) new ProductXYZR("Limits",p,ipls.ipl()); + (void) new ProductEmpty("C",q,ipls.ipl()); + (void) new ProductSingleton("C",q,ipls.ipl()); + (void) new ProductXXYAlias("C",q,ipls.ipl()); + (void) new AbsXY("A",a,ipls.ipl()); (void) new AbsXY("B",b,ipls.ipl()); (void) new AbsXY("C",c,ipls.ipl()); From 379e4367c87c043e5c5d20f2bd7e405af938c502 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 11:17:35 +0200 Subject: [PATCH 04/35] Add fixed-modulus product propagators Zdev-Change-Id: Zb9abdc3f43ea2d3f0e8841d8777d60cf54652d1b10d37d8ae2e45bbc9ba56242 --- .zd/gcd/TASKS.md | 6 +- ...d-reified-fixed-modulus-n-ary-product-c.md | 26 +- Makefile.in | 2 +- gecode/int.hh | 22 ++ gecode/int/arithmetic.cpp | 36 ++ gecode/int/arithmetic.hh | 46 +++ gecode/int/arithmetic/product-mod.hpp | 316 ++++++++++++++++++ test/int/arithmetic.cpp | 139 ++++++++ 8 files changed, 582 insertions(+), 11 deletions(-) create mode 100644 gecode/int/arithmetic/product-mod.hpp diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 0b3f8134a5..26a747737a 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,13 +3,13 @@ # Tasks: gcd - Total: 4 -- Ready: 1 +- Ready: 0 - Blocked: 0 -- Done: 3 +- Done: 4 | ID | Task | State | Blocked by | | --- | --- | --- | --- | | [gcd-001](tasks/001-implement-the-ternary-integer-gcd-constraint.md) | Implement ternary and reified integer GCD constraints | done | — | | [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | done | — | | [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | -| [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | ready | — | +| [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | diff --git a/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md b/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md index afa16596fb..f24d2d7621 100644 --- a/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md +++ b/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-004" key = "nary-product-mod" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Implement ordinary and reified fixed-modulus n-ary product constraints @@ -20,15 +20,27 @@ Users can constrain an integer result to the canonical Euclidean residue of an I ## Done when -- [ ] Documented public ordinary and Reify product_mod posting APIs accept IntVarArgs factors, a fixed int modulus, an IntVar result, and IntPropLevel and are available from gecode/int.hh. -- [ ] Posting rejects a nonpositive modulus through the repository's established integer-argument exception convention and constrains every result to the canonical range zero through modulus minus one. -- [ ] Dedicated n-ary actors implement overflow-safe modular arithmetic, sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. -- [ ] The relation uses SMT-LIB-compatible Euclidean residues, defines the empty product as one modulo the modulus, and handles modulus one, singleton, repeated, zero, signed, sparse, assigned, and aliased variables. -- [ ] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, modulus one, canonical residues for negative factors, empty and singleton arrays, zeros, sparse domains, repetition, result aliasing, and cloning without new test infrastructure. -- [ ] The affected integer library and focused tests build successfully. +- [x] Documented public ordinary and Reify product_mod posting APIs accept IntVarArgs factors, a fixed int modulus, an IntVar result, and IntPropLevel and are available from gecode/int.hh. +- [x] Posting rejects a nonpositive modulus through the repository's established integer-argument exception convention and constrains every result to the canonical range zero through modulus minus one. +- [x] Dedicated n-ary actors implement overflow-safe modular arithmetic, sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. +- [x] The relation uses SMT-LIB-compatible Euclidean residues, defines the empty product as one modulo the modulus, and handles modulus one, singleton, repeated, zero, signed, sparse, assigned, and aliased variables. +- [x] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, modulus one, canonical residues for negative factors, empty and singleton arrays, zeros, sparse domains, repetition, result aliasing, and cloning without new test infrastructure. +- [x] The affected integer library and focused tests build successfully. ## Validation - Build the configured integer library and test executable. - Run the focused ordinary and reified Int::Arithmetic::ProductMod tests. - Run the broader integer test target when available in the existing local build. + +## Result + +Implemented and independently verified ordinary and reified fixed-modulus n-ary product constraints with canonical Euclidean residues, positive-modulus validation, overflow-safe modular arithmetic, alias-aware support filtering, and correct inactive implication behavior. + +Validation: + +- Built the integer library and gecode-test successfully. +- All 19 focused Int::Arithmetic::ProductMod cases passed. +- All 473 broader Int::Arithmetic tests passed. +- Fresh install, exported-symbol/header assertions, and targeted implication/range/modulus-one/max-modulus consumer smokes passed. +- git diff --check and zd check passed. diff --git a/Makefile.in b/Makefile.in index 0a7e7353ac..8141d7ac65 100755 --- a/Makefile.in +++ b/Makefile.in @@ -352,7 +352,7 @@ INTHDR0 = \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ arithmetic/abs.hpp arithmetic/max.hpp arithmetic/argmax.hpp \ - arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/product.hpp arithmetic/divmod.hpp \ + arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/product.hpp arithmetic/product-mod.hpp arithmetic/divmod.hpp \ arithmetic/pow-ops.hpp arithmetic/pow.hpp arithmetic/nroot.hpp \ bool/or.hpp bool/eq.hpp bool/lq.hpp bool/eqv.hpp bool/base.hpp \ bool/clause.hpp bool/ite.hpp \ diff --git a/gecode/int.hh b/gecode/int.hh index 60b4215b14..0868182a29 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3080,6 +3080,28 @@ namespace Gecode { product(Home home, const IntVarArgs& x, IntVar y, Reify r, IntPropLevel ipl=IPL_DEF); + /** \brief Constrain \a y to the product of \a x modulo \a m + * + * The modulus \a m must be positive. The result uses the canonical + * Euclidean residue in the range zero through \a m minus one. The product + * of an empty array is one. + * \ingroup TaskModelInt + */ + GECODE_INT_EXPORT void + product_mod(Home home, const IntVarArgs& x, int m, IntVar y, + IntPropLevel ipl=IPL_DEF); + + /** \brief Reify whether \a y is the product of \a x modulo \a m + * + * The modulus \a m must be positive. The result uses the canonical + * Euclidean residue in the range zero through \a m minus one. The product + * of an empty array is one. + * \ingroup TaskModelInt + */ + GECODE_INT_EXPORT void + product_mod(Home home, const IntVarArgs& x, int m, IntVar y, Reify r, + IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$x_0\ \mathrm{div}\ x_1=x_2 \land x_0\ \mathrm{mod}\ x_1 = x_3\f$ * * Supports bounds consistency (\a ipl = IPL_BND, default). diff --git a/gecode/int/arithmetic.cpp b/gecode/int/arithmetic.cpp index 99d6a226fc..8bdf8eb451 100755 --- a/gecode/int/arithmetic.cpp +++ b/gecode/int/arithmetic.cpp @@ -404,6 +404,42 @@ namespace Gecode { } } + void + product_mod(Home home, const IntVarArgs& x, int m, IntVar y, + IntPropLevel) { + using namespace Int; + Limits::positive(m,"Int::product_mod"); + GECODE_POST; + IntView yv(y); + ViewArray xv(home,x); + GECODE_ES_FAIL(Arithmetic::ProductMod::post(home,xv,m,yv)); + } + + void + product_mod(Home home, const IntVarArgs& x, int m, IntVar y, Reify r, + IntPropLevel) { + using namespace Int; + Limits::positive(m,"Int::product_mod"); + GECODE_POST; + IntView yv(y); + ViewArray xv(home,x); + switch (r.mode()) { + case RM_EQV: + GECODE_ES_FAIL((Arithmetic::ReProductMod + ::post(home,xv,m,yv,r.var()))); + break; + case RM_IMP: + GECODE_ES_FAIL((Arithmetic::ReProductMod + ::post(home,xv,m,yv,r.var()))); + break; + case RM_PMI: + GECODE_ES_FAIL((Arithmetic::ReProductMod + ::post(home,xv,m,yv,r.var()))); + break; + default: GECODE_NEVER; + } + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index c41b6e1479..88ffb45207 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -902,6 +902,52 @@ namespace Gecode { namespace Int { namespace Arithmetic { #include +namespace Gecode { namespace Int { namespace Arithmetic { + + /** \brief Domain propagator for a fixed-modulus n-ary product + * \ingroup FuncIntProp + */ + class ProductMod : public NaryOnePropagator { + protected: + using NaryOnePropagator::x; + using NaryOnePropagator::y; + int m; + ProductMod(Home home, ViewArray& x, int m, IntView y); + ProductMod(Space& home, ProductMod& p); + public: + static ExecStatus post(Home home, ViewArray& x, int m, IntView y); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + }; + + /** \brief Reified domain propagator for a fixed-modulus n-ary product + * \ingroup FuncIntProp + */ + template + class ReProductMod : public Propagator { + protected: + ViewArray x; + IntView y; + BoolView b; + int m; + ReProductMod(Home home, ViewArray& x, int m, IntView y, + BoolView b); + ReProductMod(Space& home, ReProductMod& p); + public: + static ExecStatus post(Home home, ViewArray& x, int m, IntView y, + BoolView b); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual void reschedule(Space& home); + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + virtual size_t dispose(Space& home); + }; + +}}} + +#include + namespace Gecode { namespace Int { namespace Arithmetic { /** diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp new file mode 100644 index 0000000000..4458e3de5f --- /dev/null +++ b/gecode/int/arithmetic/product-mod.hpp @@ -0,0 +1,316 @@ +/* -*- 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 + +namespace Gecode { namespace Int { namespace Arithmetic { + + const unsigned long long product_mod_support_limit = 200000ULL; + + inline bool + product_mod_enumerable(const ViewArray& x, const IntView& y) { + unsigned long long n = y.size(); + for (int i=0; i product_mod_support_limit / s)) + return false; + n *= s; + } + return n <= product_mod_support_limit; + } + + /// Compute the canonical residue without signed overflow. + inline int + product_mod_value(const int* v, int n, int m) { + long long int p = 1 % m; + for (int i=0; i(v[i]) % m; + if (q < 0) + q += m; + // Both factors are smaller than Limits::max, so this fits in int64. + p = (p*q) % m; + } + return static_cast(p); + } + + inline bool + product_mod_alias_ok(const ViewArray& x, const IntView& y, + const int* v) { + for (int i=0; i& x, IntView y, int m, + bool positive, bool filter, + bool& has_true, bool& has_false) { + const int n = x.size(); + Region r; + unsigned int* size = r.alloc(n+1); + unsigned int* pos = r.alloc(n+1); + int** values = r.alloc(n+1); + unsigned long long cap64 = y.size(); + for (int i=0; i(cap64); + int** support = filter ? r.alloc(n+1) : nullptr; + + for (int i=0; i(size[i]); + unsigned int k=0; + for (ViewValues vi(x[i]); vi(); ++vi) + values[i][k++] = vi.val(); + if (filter) + support[i] = r.alloc(cap); + } + size[n] = y.size(); pos[n] = 0; + values[n] = r.alloc(size[n]); + unsigned int k=0; + for (ViewValues vi(y); vi(); ++vi) + values[n][k++] = vi.val(); + if (filter) + support[n] = r.alloc(cap); + + int* tuple = r.alloc(n+1); + unsigned int ns = 0; + has_true = has_false = false; + bool more = true; + while (more) { + for (int i=0; i<=n; i++) + tuple[i] = values[i][pos[i]]; + if (product_mod_alias_ok(x,y,tuple)) { + const bool relation = + product_mod_value(tuple,n,m) == tuple[n]; + has_true |= relation; has_false |= !relation; + if (filter && (relation == positive)) { + for (int i=0; i<=n; i++) + support[i][ns] = tuple[i]; + ns++; + } + } + int i=n; + while ((i >= 0) && (++pos[i] == size[i])) { + pos[i]=0; i--; + } + more = i >= 0; + } + + if (!filter) + return ES_OK; + if (ns == 0) + return ES_FAILED; + for (int i=0; i<=n; i++) { + std::sort(support[i],support[i]+ns); + unsigned int nu=1; + for (unsigned int j=1; j& z, int modulus, + IntView w) + : NaryOnePropagator(home,z,w), m(modulus) {} + + inline ExecStatus + ProductMod::post(Home home, ViewArray& x, int m, IntView y) { + GECODE_ME_CHECK(y.gq(home,0)); + GECODE_ME_CHECK(y.lq(home,m-1)); + if (x.size() == 0) { + GECODE_ME_CHECK(y.eq(home,1 % m)); + return ES_OK; + } + (void) new (home) ProductMod(home,x,m,y); + return ES_OK; + } + + forceinline + ProductMod::ProductMod(Space& home, ProductMod& p) + : NaryOnePropagator(home,p), m(p.m) {} + + forceinline Actor* + ProductMod::copy(Space& home) { + return new (home) ProductMod(home,*this); + } + + forceinline PropCost + ProductMod::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+1); + } + + inline ExecStatus + ProductMod::propagate(Space& home, const ModEventDelta&) { + if (product_mod_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_mod_supports(home,x,y,m,true,true,ht,hf)); + } + bool assigned = y.assigned(); + for (int i=0; assigned && (i + forceinline + ReProductMod::ReProductMod(Home home, ViewArray& z, + int modulus, IntView w, BoolView c) + : Propagator(home), x(z), y(w), b(c), m(modulus) { + x.subscribe(home,*this,PC_INT_DOM); + y.subscribe(home,*this,PC_INT_DOM); + b.subscribe(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProductMod::post(Home home, ViewArray& x, int m, IntView y, + BoolView b) { + if (b.one() && (rm == RM_PMI)) + return ES_OK; + if (b.zero() && (rm == RM_IMP)) + return ES_OK; + if (x.size() == 0) { + const int identity = 1 % m; + const bool t = y.assigned() ? (y.val() == identity) : false; + if (b.one() && (rm != RM_PMI)) { + GECODE_ME_CHECK(y.eq(home,identity)); return ES_OK; + } + if (b.zero() && (rm != RM_IMP)) { + GECODE_ME_CHECK(y.nq(home,identity)); return ES_OK; + } + if (!y.in(identity)) { + if (rm != RM_PMI) + GECODE_ME_CHECK(b.zero(home)); + return ES_OK; + } + if (t) { + if (rm != RM_IMP) + GECODE_ME_CHECK(b.one(home)); + return ES_OK; + } + } + (void) new (home) ReProductMod(home,x,m,y,b); + return ES_OK; + } + + template + forceinline + ReProductMod::ReProductMod(Space& home, ReProductMod& p) + : Propagator(home,p), m(p.m) { + x.update(home,p.x); y.update(home,p.y); b.update(home,p.b); + } + + template + forceinline Actor* + ReProductMod::copy(Space& home) { + return new (home) ReProductMod(home,*this); + } + + template + forceinline PropCost + ReProductMod::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+2); + } + + template + forceinline void + ReProductMod::reschedule(Space& home) { + x.reschedule(home,*this,PC_INT_DOM); + y.reschedule(home,*this,PC_INT_DOM); + b.reschedule(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProductMod::propagate(Space& home, const ModEventDelta&) { + if (b.one()) { + if (rm == RM_PMI) + return home.ES_SUBSUMED(*this); + GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m,y)); + } + if (b.zero()) { + if (rm == RM_IMP) + return home.ES_SUBSUMED(*this); + if (product_mod_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_mod_supports + (home,x,y,m,false,true,ht,hf)); + if (!ht) + return home.ES_SUBSUMED(*this); + } + return ES_FIX; + } + if (product_mod_enumerable(x,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_mod_supports + (home,x,y,m,true,false,ht,hf)); + if (!ht) { + if (rm != RM_PMI) + GECODE_ME_CHECK(b.zero_none(home)); + return home.ES_SUBSUMED(*this); + } + if (!hf) { + if (rm != RM_IMP) + GECODE_ME_CHECK(b.one_none(home)); + return home.ES_SUBSUMED(*this); + } + } + return ES_FIX; + } + + template + forceinline size_t + ReProductMod::dispose(Space& home) { + x.cancel(home,*this,PC_INT_DOM); + y.cancel(home,*this,PC_INT_DOM); + b.cancel(home,*this,PC_BOOL_VAL); + (void) Propagator::dispose(home); + return sizeof(*this); + } + +}}} diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 3dc3eba479..d097a97487 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -296,6 +296,137 @@ namespace Test { namespace Int { } }; + /// Compute a canonical modular product for testing. + int product_mod_value(const Assignment& x, int n, int m) { + long long int p = 1 % m; + for (int i=0; i(x[i]) % m; + if (q < 0) + q += m; + p = (p*q) % m; + } + return static_cast(p); + } + + /// %Test for an ordinary and reified three-factor modular product + class ProductModXYZR : public Test { + protected: + int m; + public: + ProductModXYZR(const std::string& s, const Gecode::IntSet& d, + int m0, Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductMod::XYZR::"+str(ipl)+"::"+s, + 4,d,true,ipl), m(m0) {} + virtual bool solution(const Assignment& x) const { + return product_mod_value(x,3,m) == x[3]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[1],x[2]}), + m,x[3],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[1],x[2]}), + m,x[3],r,ipl); + } + }; + + /// %Test for the empty modular product, including modulus one + class ProductModEmpty : public Test { + protected: + int m; + public: + ProductModEmpty(const std::string& s, const Gecode::IntSet& d, + int m0, Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductMod::Empty::"+str(ipl)+"::"+s, + 1,d,true,ipl), m(m0) {} + virtual bool solution(const Assignment& x) const { + return x[0] == (1 % m); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs(),m,x[0],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs(),m,x[0],r,ipl); + } + }; + + /// %Test for a singleton modular product + class ProductModSingleton : public Test { + protected: + int m; + public: + ProductModSingleton(const std::string& s, const Gecode::IntSet& d, + int m0, Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductMod::Singleton::"+str(ipl)+"::"+s, + 2,d,true,ipl), m(m0) {} + virtual bool solution(const Assignment& x) const { + int r = x[0] % m; + if (r < 0) + r += m; + return r == x[1]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}),m,x[1],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}),m,x[1],r,ipl); + } + }; + + /// %Test repeated factors with the result aliased to a factor + class ProductModXXYAlias : public Test { + protected: + int m; + public: + ProductModXXYAlias(const std::string& s, const Gecode::IntSet& d, + int m0, Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductMod::XXYAlias::"+str(ipl)+"::"+s, + 2,d,true,ipl), m(m0) {} + virtual bool solution(const Assignment& x) const { + long long int a = x[0] % m; + long long int b = x[1] % m; + if (a < 0) a += m; + if (b < 0) b += m; + return (((a*a) % m)*b) % m == x[1]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[0],x[1]}), + m,x[1],ipl); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[0],x[1]}), + m,x[1],r,ipl); + } + }; + + /// %Test that a nonpositive modular-product modulus is rejected + class ProductModInvalidModulus : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + ProductModInvalidModulus(void) + : ::Test::Base("Int::Arithmetic::ProductMod::InvalidModulus") {} + virtual bool run(void) { + TestSpace home; + Gecode::IntVar y(home,0,1); + for (int m=0; m>=-1; m--) { + try { + Gecode::product_mod(home,Gecode::IntVarArgs(),m,y); + return false; + } catch (const Gecode::Int::OutOfLimits&) { + } + } + return true; + } + }; + /// %Test for multiplication constraint class MultXYZ : public Test { public: @@ -1372,6 +1503,13 @@ namespace Test { namespace Int { (void) new ProductSingleton("C",q,ipls.ipl()); (void) new ProductXXYAlias("C",q,ipls.ipl()); + (void) new ProductModXYZR("C",q,5,ipls.ipl()); + (void) new ProductModXYZR("Sparse",g,7,ipls.ipl()); + (void) new ProductModEmpty("C",q,5,ipls.ipl()); + (void) new ProductModEmpty("ModulusOne",q,1,ipls.ipl()); + (void) new ProductModSingleton("C",g,5,ipls.ipl()); + (void) new ProductModXXYAlias("C",q,5,ipls.ipl()); + (void) new AbsXY("A",a,ipls.ipl()); (void) new AbsXY("B",b,ipls.ipl()); (void) new AbsXY("C",c,ipls.ipl()); @@ -1521,6 +1659,7 @@ namespace Test { namespace Int { (void) new ArgMinBool(i,1,false); (void) new ArgMinBoolShared(i,false); } + (void) new ProductModInvalidModulus; } }; From d743d30225d6a6ad822866c0ff03ea65697f42f3 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 11:52:49 +0200 Subject: [PATCH 05/35] Use bounds propagation for n-ary product Zdev-Change-Id: Z8b715e43b5f76417885046356ccbf2977d56af615849fca09606fa66134384fd --- .zd/gcd/TASKS.md | 5 +- .zd/gcd/brief.md | 3 + ...-support-enumeration-with-bounds-propag.md | 45 +++ gecode/int/arithmetic.hh | 8 +- gecode/int/arithmetic/product.hpp | 263 +++++++++--------- test/int/arithmetic.cpp | 46 ++- 6 files changed, 235 insertions(+), 135 deletions(-) create mode 100644 .zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 26a747737a..63291b438d 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -2,10 +2,10 @@ # Tasks: gcd -- Total: 4 +- Total: 5 - Ready: 0 - Blocked: 0 -- Done: 4 +- Done: 5 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -13,3 +13,4 @@ | [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | done | — | | [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | +| [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index 1eafced144..cf8b4ca65d 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -74,6 +74,9 @@ and focused regression coverage. `mod` convention. - Pair each ordinary n-ary product relation with a reified overload; reified GCD means reifying the full proposition `z = gcd(x,y)`. +- Propagate the positive n-ary exact-product relation from variable bounds in + general, without making useful propagation depend on Cartesian support + enumeration or a small-domain tuple threshold. ## Open questions diff --git a/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md b/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md new file mode 100644 index 0000000000..26de2991c6 --- /dev/null +++ b/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md @@ -0,0 +1,45 @@ ++++ +schema_version = 1 +id = "gcd-005" +key = "product-bounds" +area = "gcd" +status = "done" +blocked_by = [] ++++ +# Replace n-ary product support enumeration with bounds propagation + +## Outcome + +The positive n-ary exact-product relation performs sound, useful bounds propagation for general integer domains without enumerating Cartesian supports. + +## Boundaries + +- Keep the public product API and exact mathematical semantics unchanged. +- Do not change product_mod or unrelated arithmetic propagators. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [x] The ordinary Product actor derives sound bounds for the result and factors using overflow-safe interval arithmetic rather than Cartesian support enumeration. +- [x] Positive reified product cases rewrite to or otherwise use the same bounds propagator, while negative and undecided reification modes remain sound. +- [x] Subscriptions, propagation conditions, costs, fixpoint status, cloning, aliasing, zero crossing, signs, and representable-limit behavior follow Gecode actor conventions. +- [x] Focused regression tests demonstrate useful bounds pruning on domains whose Cartesian size exceeds the former support-enumeration threshold. +- [x] The affected integer library and focused product tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run the focused Int::Arithmetic::Product tests with multiple iterations. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. + +## Result + +Replaced exact n-ary product Cartesian support enumeration with independently verified overflow-safe forward and backward bounds propagation. + +Validation: + +- Built gecodeint_shared and gecode-test successfully. +- All 19 exact Int::Arithmetic::Product cases passed for five iterations. +- Broader Int::Arithmetic tests and targeted signed, zero-after-overflow, and nonrepresentable-product checks passed. +- git diff --check and zd check gcd passed. diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 88ffb45207..385f60c5b3 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -861,13 +861,13 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { - /** \brief Domain propagator for an exact n-ary product + /** \brief Bounds propagator for an exact n-ary product * \ingroup FuncIntProp */ - class Product : public NaryOnePropagator { + class Product : public NaryOnePropagator { protected: - using NaryOnePropagator::x; - using NaryOnePropagator::y; + using NaryOnePropagator::x; + using NaryOnePropagator::y; Product(Home home, ViewArray& x, IntView y); Product(Space& home, Product& p); public: diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index e141237699..ef72d60abb 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -34,19 +34,39 @@ namespace Gecode { namespace Int { namespace Arithmetic { - const unsigned long long product_support_limit = 200000ULL; + /// A representable, conservative interval for an exact product. + struct ProductInterval { + int min; + int max; + }; - /// Return whether the Cartesian product can be enumerated safely. - inline bool - product_enumerable(const ViewArray& x, const IntView& y) { - unsigned long long n = y.size(); - for (int i=0; i product_support_limit / s)) - return false; - n *= s; - } - return n <= product_support_limit; + /// Multiply two representable intervals, clipping out-of-range endpoints. + forceinline ProductInterval + product_interval_mul(ProductInterval a, int bmin, int bmax) { + long long int p[4] = { + static_cast(a.min) * bmin, + static_cast(a.min) * bmax, + static_cast(a.max) * bmin, + static_cast(a.max) * bmax + }; + long long int l = *std::min_element(p,p+4); + long long int u = *std::max_element(p,p+4); + l = std::max(static_cast(Limits::min), + std::min(l,static_cast(Limits::max))); + u = std::max(static_cast(Limits::min), + std::min(u,static_cast(Limits::max))); + ProductInterval r = {static_cast(l),static_cast(u)}; + return r; + } + + /// Compute product bounds, optionally omitting one factor. + inline ProductInterval + product_interval(const ViewArray& x, int omit=-1) { + ProductInterval r = {1,1}; + for (int i=0; i& x, const IntView& y, - const int* v) { - for (int i=0; i& x, IntView y, - bool positive, bool filter, - bool& has_true, bool& has_false) { - const int n = x.size(); + /// Determine the relation when bounds or assignments prove it. + inline RelTest + product_status(const ViewArray& x, const IntView& y) { + ProductInterval p = product_interval(x); + if ((p.max < y.min()) || (p.min > y.max())) + return RT_FALSE; + bool assigned = true; + for (int i=0; assigned && (i(n+1); - unsigned int* pos = r.alloc(n+1); - int** values = r.alloc(n+1); - unsigned long long cap64 = y.size(); - for (int i=0; i(cap64); - int** support = filter ? r.alloc(n+1) : nullptr; - - for (int i=0; i(size[i]); - unsigned int k=0; - for (ViewValues vi(x[i]); vi(); ++vi) - values[i][k++] = vi.val(); - if (filter) support[i] = r.alloc(cap); - } - size[n] = y.size(); pos[n] = 0; - values[n] = r.alloc(size[n]); - unsigned int k=0; - for (ViewValues vi(y); vi(); ++vi) - values[n][k++] = vi.val(); - if (filter) support[n] = r.alloc(cap); - - int* tuple = r.alloc(n+1); - unsigned int ns = 0; - has_true = has_false = false; - bool more = true; - while (more) { - for (int i=0; i<=n; i++) tuple[i] = values[i][pos[i]]; - if (product_alias_ok(x,y,tuple)) { - int p; - bool relation = product_value(tuple,n,p) && (p == tuple[n]); - has_true |= relation; has_false |= !relation; - if (filter && (relation == positive)) { - for (int i=0; i<=n; i++) support[i][ns] = tuple[i]; - ns++; - } - } - int i=n; - while ((i >= 0) && (++pos[i] == size[i])) { - pos[i]=0; i--; - } - more = i >= 0; - } - - if (!filter) - return ES_OK; - if (ns == 0) - return ES_FAILED; - for (int i=0; i<=n; i++) { - std::sort(support[i],support[i]+ns); - unsigned int nu=1; - for (unsigned int j=1; j(x.size()); + for (int i=0; i& z, IntView w) - : NaryOnePropagator(home,z,w) {} + : NaryOnePropagator(home,z,w) {} inline ExecStatus Product::post(Home home, ViewArray& x, IntView y) { @@ -173,7 +125,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline Product::Product(Space& home, Product& p) - : NaryOnePropagator(home,p) {} + : NaryOnePropagator(home,p) {} forceinline Actor* Product::copy(Space& home) { @@ -182,20 +134,83 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline PropCost Product::cost(const Space&, const ModEventDelta&) const { - return PropCost::linear(PropCost::HI,x.size()+1); + return PropCost::quadratic(PropCost::LO,x.size()+1); } inline ExecStatus Product::propagate(Space& home, const ModEventDelta&) { - if (product_enumerable(x,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_supports(home,x,y,true,true,ht,hf)); - } - bool assigned = y.assigned(); - for (int i=0; assigned && (i 0) || (q.max < 0)) { + long long int c[4] = { + ceil_div_xx(static_cast(y.min()), + static_cast(q.min)), + ceil_div_xx(static_cast(y.min()), + static_cast(q.max)), + ceil_div_xx(static_cast(y.max()), + static_cast(q.min)), + ceil_div_xx(static_cast(y.max()), + static_cast(q.max)) + }; + long long int f[4] = { + floor_div_xx(static_cast(y.min()), + static_cast(q.min)), + floor_div_xx(static_cast(y.min()), + static_cast(q.max)), + floor_div_xx(static_cast(y.max()), + static_cast(q.min)), + floor_div_xx(static_cast(y.max()), + static_cast(q.max)) + }; + long long int l = *std::min_element(c,c+4); + long long int u = *std::max_element(f,f+4); + l = std::max(l,static_cast(Limits::min)); + u = std::min(u,static_cast(Limits::max)); + if (l > u) + return ES_FAILED; + { + ModEvent me = x[i].gq(home,static_cast(l)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me = x[i].lq(home,static_cast(u)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + } + } + } while (modified); + + bool factors_assigned = true; + for (int i=0; factors_assigned && (i(x.size()); + for (int i=0; i diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index d097a97487..8848fb5f4a 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -221,7 +221,7 @@ namespace Test { namespace Int { ProductXYZR(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::XYZR::"+str(ipl)+"::"+s, - 4,d,true,ipl) {} + 4,d,true,ipl) { contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { int p; return product_value(x,3,p) && (p == x[3]); @@ -243,7 +243,7 @@ namespace Test { namespace Int { ProductEmpty(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::Empty::"+str(ipl)+"::"+s, - 1,d,true,ipl) {} + 1,d,true,ipl) { contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == 1; } @@ -262,7 +262,7 @@ namespace Test { namespace Int { ProductSingleton(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::Singleton::"+str(ipl)+"::"+s, - 2,d,true,ipl) {} + 2,d,true,ipl) { contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == x[1]; } @@ -281,7 +281,7 @@ namespace Test { namespace Int { ProductXXYAlias(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::XXYAlias::"+str(ipl)+"::"+s, - 2,d,true,ipl) {} + 2,d,true,ipl) { contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { long long int p = static_cast(x[0]) * x[0] * x[1]; return (p >= Gecode::Int::Limits::min) && @@ -296,6 +296,43 @@ namespace Test { namespace Int { } }; + /// %Test product bounds propagation beyond the support-enumeration cap + class ProductBoundsLarge : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + ProductBoundsLarge(void) + : ::Test::Base("Int::Arithmetic::Product::BoundsLarge") {} + virtual bool run(void) { + using namespace Gecode; + { + TestSpace home; + IntVarArgs x(home,3,2,100); + IntVar y(home,0,Gecode::Int::Limits::max); + BoolVar b(home,1,1); + product(home,x,y,Reify(b)); + if ((home.status() == SS_FAILED) || + (y.min() != 8) || (y.max() != 1000000)) + return false; + } + { + TestSpace home; + IntVarArgs x(home,3,10,100); + IntVar y(home,1000,1000); + product(home,x,y); + if (home.status() == SS_FAILED) + return false; + for (int i=0; i Date: Wed, 12 Aug 2026 12:11:48 +0200 Subject: [PATCH 06/35] Plan variable-modulus product propagators Zdev-Change-Id: Zf0e584ca47fe274025cb88164a652e1d50742bd9bb9f23714881ae5026f218a1 --- .zd/gcd/TASKS.md | 8 +++-- .zd/gcd/area.toml | 2 +- .zd/gcd/brief.md | 23 +++++++----- ...-ary-product-mod-with-an-intvar-modulus.md | 36 +++++++++++++++++++ ...-ary-product-mod-with-an-intvar-modulus.md | 35 ++++++++++++++++++ 5 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 .zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md create mode 100644 .zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 63291b438d..a5e4f3bced 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -2,9 +2,9 @@ # Tasks: gcd -- Total: 5 -- Ready: 0 -- Blocked: 0 +- Total: 7 +- Ready: 1 +- Blocked: 1 - Done: 5 | ID | Task | State | Blocked by | @@ -14,3 +14,5 @@ | [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | | [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | +| [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | ready | — | +| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | blocked | gcd-006 | diff --git a/.zd/gcd/area.toml b/.zd/gcd/area.toml index 6e68b0d627..6db75d1d40 100644 --- a/.zd/gcd/area.toml +++ b/.zd/gcd/area.toml @@ -1,6 +1,6 @@ schema_version = 1 tag = "gcd" title = "Integer number-theoretic and product propagators" -objective = "Expose ordinary and reified integer constraints for GCD, divisibility, n-ary product, and fixed-modulus n-ary product, with sound propagation, public APIs, and focused regression coverage." +objective = "Expose ordinary and reified integer constraints for GCD, divisibility, n-ary product, and fixed- or variable-modulus n-ary product, with sound propagation, public APIs, and focused regression coverage." branch = "feature/gcd" base_commit = "e1ec3da365f6c927d937b5ae9266d892a1cb9807" diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index cf8b4ca65d..e2e6e35480 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -3,8 +3,8 @@ ## Objective Expose ordinary and reified integer constraints for GCD, divisibility, n-ary -product, and fixed-modulus n-ary product, with sound propagation, public APIs, -and focused regression coverage. +product, and fixed- or variable-modulus n-ary product, with sound propagation, +public APIs, and focused regression coverage. ## Success @@ -23,6 +23,8 @@ and focused regression coverage. - The public integer API can post ordinary and reified `product_mod(home, factors, modulus, result, ..., ipl)` constraints for a fixed positive integer modulus and canonical Euclidean residue. +- The public integer API can also post ordinary and reified `product_mod` + constraints whose positive modulus is an integer variable. - All constraints propagate soundly across negative, zero, sparse, assigned, empty, repeated, and aliased variable domains as applicable; their actors clone, subscribe, reschedule, rewrite, and subsume according to existing @@ -40,8 +42,8 @@ and focused regression coverage. - Do not introduce a new test harness or broad solver-wide test matrix. - Only the reified `divides` API is in scope; a separate non-reified overload is not required. -- `product_mod` accepts a fixed integer modulus only; an `IntVar` modulus - overload is outside scope. +- Variable-modulus `product_mod` accepts one `IntVar` modulus; MiniModel and + FlatZinc exposure remain outside scope. ## Terms @@ -55,7 +57,7 @@ and focused regression coverage. the empty product equal to one. Implementations must not silently overflow while computing supports or bounds. - `product_mod(x,m)` is the unique residue `r` such that - `r` is congruent to `product(x)` modulo the fixed positive integer `m` and + `m > 0`, `r` is congruent to `product(x)` modulo `m`, and `0 <= r < m`. Thus `product_mod([],m) = 1 mod m`, including zero when `m=1`. - Reification follows Gecode's `Reify` modes: equivalence (`RM_EQV`), forward implication (`RM_IMP`), and reverse implication (`RM_PMI`). @@ -77,6 +79,9 @@ and focused regression coverage. - Propagate the positive n-ary exact-product relation from variable bounds in general, without making useful propagation depend on Cartesian support enumeration or a small-domain tuple threshold. +- For a variable modulus, positivity is part of the `product_mod` relation. + The ordinary constraint enforces it. Reification controls the full + proposition, so inactive implications do not narrow the modulus or result. ## Open questions @@ -93,9 +98,11 @@ normalization, the `(0,0)` case, and all reification modes. For `product`, cover empty and singleton arrays, zeros, signs, repeated or aliased factors, result aliasing, exact-product overflow boundaries, and all reification modes. For `product_mod`, cover modulus one, negative factors, canonical residues, empty -and singleton arrays, aliasing, and all reification modes. Reuse existing -arithmetic test helpers and commands; do not create new test infrastructure or -an exhaustive large-domain performance matrix. +and singleton arrays, aliasing, and all reification modes. Variable-modulus +coverage also includes nonpositive candidates, sparse modulus domains, +modulus/result or modulus/factor aliasing, and inactive implication behavior. +Reuse existing arithmetic test helpers and commands; do not create new test +infrastructure or an exhaustive large-domain performance matrix. ## Validation diff --git a/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md new file mode 100644 index 0000000000..6265e6a634 --- /dev/null +++ b/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md @@ -0,0 +1,36 @@ ++++ +schema_version = 1 +id = "gcd-006" +key = "variable-product-mod" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Implement n-ary product_mod with an IntVar modulus + +## Outcome + +Users can constrain an integer result to the canonical Euclidean residue of an n-ary product under a positive integer-variable modulus. + +## Boundaries + +- Add the ordinary IntVar-modulus overload without changing the existing fixed-int overload's behavior. +- Keep this slice within integer arithmetic APIs and propagators; do not add MiniModel or FlatZinc support. +- Leave reification to the dependent variable-product-mod-reified task. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [ ] A documented public ordinary product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, and IntPropLevel. +- [ ] The ordinary constraint enforces modulus > 0 and the canonical result range 0 <= result < modulus. +- [ ] A dedicated or appropriately shared actor propagates soundly for assigned and unassigned moduli, negative and zero factors, empty and singleton products, sparse domains, repeated variables, aliases, modulus one, and representable limits. +- [ ] Actor posting, subscriptions, propagation, cloning, rescheduling, disposal, rewriting, and subsumption follow existing Gecode conventions without unsafe arithmetic. +- [ ] Focused existing-harness tests cover ordinary variable-modulus solutions and propagation, including nonpositive modulus removal and aliases. +- [ ] The affected integer library and focused ProductMod tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run the focused ordinary variable-modulus ProductMod tests. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. diff --git a/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md new file mode 100644 index 0000000000..6f4b84428e --- /dev/null +++ b/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md @@ -0,0 +1,35 @@ ++++ +schema_version = 1 +id = "gcd-007" +key = "variable-product-mod-reified" +area = "gcd" +status = "open" +blocked_by = ["gcd-006"] ++++ +# Implement reified n-ary product_mod with an IntVar modulus + +## Outcome + +Users can reify the full positive-variable-modulus product_mod proposition in every Gecode reification mode. + +## Boundaries + +- Build on the ordinary variable-modulus relation and preserve both existing fixed-modulus overloads. +- Keep this slice within integer arithmetic APIs and propagators; do not add MiniModel or FlatZinc support. +- Follow the brief's focused testing level and existing reified arithmetic-test patterns. + +## Done when + +- [ ] A documented public product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, Reify, and IntPropLevel. +- [ ] The reified proposition includes modulus > 0, canonical range, and congruence, with correct RM_EQV, RM_IMP, and RM_PMI behavior. +- [ ] Inactive implications do not narrow the modulus, factors, or result; positive control rewrites to or shares the ordinary variable-modulus propagator. +- [ ] The actor handles fixed and unfixed Boolean controls, nonpositive modulus candidates, sparse domains, modulus one, empty and singleton products, signs, zeros, aliases, cloning, rewriting, and subsumption soundly. +- [ ] Focused existing-harness tests cover all reification modes and fixed/unfixed controls, including inactive implications and false supports from nonpositive moduli or noncanonical results. +- [ ] The affected integer library and focused ProductMod tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run the focused reified variable-modulus ProductMod tests with all reification modes. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. From d74327cf81edc449407049f6138f9ee324cbf3ac Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 13:04:24 +0200 Subject: [PATCH 07/35] Add variable-modulus product propagator Zdev-Change-Id: Z173281beec8d8c90d2cee6d137e40c90e7670c7e0f1660e1fc8bbbb3f2dc1c46 --- .zd/gcd/TASKS.md | 8 +- ...-ary-product-mod-with-an-intvar-modulus.md | 25 ++- gecode/int.hh | 11 ++ gecode/int/arithmetic.cpp | 9 + gecode/int/arithmetic.hh | 20 ++ gecode/int/arithmetic/product-mod.hpp | 187 ++++++++++++++++++ test/int/arithmetic.cpp | 171 ++++++++++++++++ 7 files changed, 420 insertions(+), 11 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index a5e4f3bced..d211bb251c 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -4,8 +4,8 @@ - Total: 7 - Ready: 1 -- Blocked: 1 -- Done: 5 +- Blocked: 0 +- Done: 6 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -14,5 +14,5 @@ | [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | | [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | -| [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | ready | — | -| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | blocked | gcd-006 | +| [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | +| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | ready | gcd-006 | diff --git a/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md index 6265e6a634..2fdfcb92d2 100644 --- a/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md +++ b/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-006" key = "variable-product-mod" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Implement n-ary product_mod with an IntVar modulus @@ -21,12 +21,12 @@ Users can constrain an integer result to the canonical Euclidean residue of an n ## Done when -- [ ] A documented public ordinary product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, and IntPropLevel. -- [ ] The ordinary constraint enforces modulus > 0 and the canonical result range 0 <= result < modulus. -- [ ] A dedicated or appropriately shared actor propagates soundly for assigned and unassigned moduli, negative and zero factors, empty and singleton products, sparse domains, repeated variables, aliases, modulus one, and representable limits. -- [ ] Actor posting, subscriptions, propagation, cloning, rescheduling, disposal, rewriting, and subsumption follow existing Gecode conventions without unsafe arithmetic. -- [ ] Focused existing-harness tests cover ordinary variable-modulus solutions and propagation, including nonpositive modulus removal and aliases. -- [ ] The affected integer library and focused ProductMod tests build and pass. +- [x] A documented public ordinary product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, and IntPropLevel. +- [x] The ordinary constraint enforces modulus > 0 and the canonical result range 0 <= result < modulus. +- [x] A dedicated or appropriately shared actor propagates soundly for assigned and unassigned moduli, negative and zero factors, empty and singleton products, sparse domains, repeated variables, aliases, modulus one, and representable limits. +- [x] Actor posting, subscriptions, propagation, cloning, rescheduling, disposal, rewriting, and subsumption follow existing Gecode conventions without unsafe arithmetic. +- [x] Focused existing-harness tests cover ordinary variable-modulus solutions and propagation, including nonpositive modulus removal and aliases. +- [x] The affected integer library and focused ProductMod tests build and pass. ## Validation @@ -34,3 +34,14 @@ Users can constrain an integer result to the canonical Euclidean residue of an n - Run the focused ordinary variable-modulus ProductMod tests. - Run the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Implemented and independently verified ordinary n-ary product_mod with a positive IntVar modulus and canonical Euclidean result. + +Validation: + +- Built gecode-test and the affected integer library successfully. +- All 25 ProductModVar cases and all 19 fixed ProductMod regressions passed. +- Broader Int::Arithmetic tests, fresh install, and installed-consumer smoke passed. +- git diff --check and zd check gcd passed. diff --git a/gecode/int.hh b/gecode/int.hh index 0868182a29..aeb95fb9ab 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3102,6 +3102,17 @@ namespace Gecode { product_mod(Home home, const IntVarArgs& x, int m, IntVar y, Reify r, IntPropLevel ipl=IPL_DEF); + /** \brief Constrain \a y to the product of \a x modulo \a m + * + * The variable modulus \a m is constrained to be positive and \a y uses + * the canonical Euclidean residue, so that \f$0\leq y xv(home,x); + GECODE_ES_FAIL(Arithmetic::ProductModVar::post(home,xv,m,y)); + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 385f60c5b3..79c1e026da 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -921,6 +921,26 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual ExecStatus propagate(Space& home, const ModEventDelta& med); }; + /** \brief Domain propagator for a variable-modulus n-ary product + * \ingroup FuncIntProp + */ + class ProductModVar : public Propagator { + protected: + ViewArray x; + IntView m; + IntView y; + ProductModVar(Home home, ViewArray& x, IntView m, IntView y); + ProductModVar(Space& home, ProductModVar& p); + public: + static ExecStatus post(Home home, ViewArray& x, + IntView m, IntView y); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual void reschedule(Space& home); + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + virtual size_t dispose(Space& home); + }; + /** \brief Reified domain propagator for a fixed-modulus n-ary product * \ingroup FuncIntProp */ diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 4458e3de5f..3245aa39d2 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -196,6 +196,193 @@ namespace Gecode { namespace Int { namespace Arithmetic { return ES_FIX; } + inline bool + product_mod_var_enumerable(const ViewArray& x, + const IntView& m, const IntView& y) { + unsigned long long n = static_cast(m.size()) * y.size(); + if (n > product_mod_support_limit) + return false; + for (int i=0; i product_mod_support_limit / s)) + return false; + n *= s; + } + return n <= product_mod_support_limit; + } + + inline bool + product_mod_var_alias_ok(const ViewArray& x, + const IntView& m, const IntView& y, + const int* v) { + const int mi = x.size(); + const int yi = mi+1; + if ((m == y) && (v[mi] != v[yi])) + return false; + for (int i=0; i& x, + IntView m, IntView y) { + const int n = x.size(); + const int nv = n+2; + Region r; + unsigned int* size = r.alloc(nv); + unsigned int* pos = r.alloc(nv); + int** values = r.alloc(nv); + unsigned long long cap64 = static_cast(m.size()) * + y.size(); + for (int i=0; i(cap64); + int** support = r.alloc(nv); + + for (int i=0; i(size[i]); + unsigned int k=0; + for (ViewValues vi(x[i]); vi(); ++vi) + values[i][k++] = vi.val(); + support[i] = r.alloc(cap); + } + size[n] = m.size(); pos[n] = 0; + values[n] = r.alloc(size[n]); + unsigned int k=0; + for (ViewValues vi(m); vi(); ++vi) + values[n][k++] = vi.val(); + support[n] = r.alloc(cap); + size[n+1] = y.size(); pos[n+1] = 0; + values[n+1] = r.alloc(size[n+1]); + k=0; + for (ViewValues vi(y); vi(); ++vi) + values[n+1][k++] = vi.val(); + support[n+1] = r.alloc(cap); + + int* tuple = r.alloc(nv); + unsigned int ns = 0; + bool more = true; + while (more) { + for (int i=0; i 0) && + (product_mod_value(tuple,n,tuple[n]) == tuple[n+1])) { + for (int i=0; i= 0) && (++pos[i] == size[i])) { + pos[i]=0; i--; + } + more = i >= 0; + } + + if (ns == 0) + return ES_FAILED; + for (int i=0; i& z, + IntView modulus, IntView w) + : Propagator(home), x(z), m(modulus), y(w) { + x.subscribe(home,*this,PC_INT_DOM); + m.subscribe(home,*this,PC_INT_DOM); + y.subscribe(home,*this,PC_INT_DOM); + } + + inline ExecStatus + ProductModVar::post(Home home, ViewArray& x, IntView m, IntView y) { + if (m == y) + return ES_FAILED; + GECODE_ME_CHECK(m.gq(home,1)); + GECODE_ME_CHECK(y.gq(home,0)); + GECODE_ME_CHECK(y.lq(home,m.max()-1)); + GECODE_ME_CHECK(m.gq(home,y.min()+1)); + if (m.assigned()) + return ProductMod::post(home,x,m.val(),y); + (void) new (home) ProductModVar(home,x,m,y); + return ES_OK; + } + + forceinline + ProductModVar::ProductModVar(Space& home, ProductModVar& p) + : Propagator(home,p) { + x.update(home,p.x); m.update(home,p.m); y.update(home,p.y); + } + + forceinline Actor* + ProductModVar::copy(Space& home) { + return new (home) ProductModVar(home,*this); + } + + forceinline PropCost + ProductModVar::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+2); + } + + forceinline void + ProductModVar::reschedule(Space& home) { + x.reschedule(home,*this,PC_INT_DOM); + m.reschedule(home,*this,PC_INT_DOM); + y.reschedule(home,*this,PC_INT_DOM); + } + + inline ExecStatus + ProductModVar::propagate(Space& home, const ModEventDelta&) { + GECODE_ME_CHECK(m.gq(home,1)); + GECODE_ME_CHECK(y.gq(home,0)); + GECODE_ME_CHECK(y.lq(home,m.max()-1)); + GECODE_ME_CHECK(m.gq(home,y.min()+1)); + if (m.assigned()) + GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); + if (product_mod_var_enumerable(x,m,y)) + GECODE_ES_CHECK(product_mod_var_supports(home,x,m,y)); + if (m.assigned()) + GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); + bool assigned = m.assigned() && y.assigned(); + for (int i=0; assigned && (i forceinline ReProductMod::ReProductMod(Home home, ViewArray& z, diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 8848fb5f4a..f12851ceba 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -440,6 +440,167 @@ namespace Test { namespace Int { } }; + /// %Test for an ordinary two-factor product with a variable modulus + class ProductModVarXYMR : public Test { + public: + ProductModVarXYMR(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::XYMR::"+str(ipl)+"::"+s, + 4,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + return (x[2] > 0) && + (product_mod_value(x,2,x[2]) == x[3]); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[1]}), + x[2],x[3],ipl); + } + }; + + /// %Test for the empty product with a variable modulus + class ProductModVarEmpty : public Test { + public: + ProductModVarEmpty(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::Empty::"+str(ipl)+"::"+s, + 2,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + return (x[0] > 0) && (x[1] == (1 % x[0])); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs(),x[0],x[1],ipl); + } + }; + + /// %Test for a singleton product with a variable modulus + class ProductModVarSingleton : public Test { + public: + ProductModVarSingleton(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::Singleton::"+str(ipl)+"::"+s, + 3,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + if (x[1] <= 0) + return false; + int r = x[0] % x[1]; + if (r < 0) + r += x[1]; + return r == x[2]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[2],ipl); + } + }; + + /// %Test repeated factors with a variable modulus + class ProductModVarRepeated : public Test { + public: + ProductModVarRepeated(const std::string& s, const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::Repeated::"+str(ipl)+"::"+s, + 3,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + if (x[1] <= 0) + return false; + long long int a = x[0] % x[1]; + if (a < 0) a += x[1]; + return (a*a) % x[1] == x[2]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[0]}), + x[1],x[2],ipl); + } + }; + + /// %Test modulus/factor aliasing + class ProductModVarModFactorAlias : public Test { + public: + ProductModVarModFactorAlias(const std::string& s, + const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::ModFactorAlias::"+ + str(ipl)+"::"+s,2,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + return (x[0] > 0) && (x[1] == 0); + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[0],x[1],ipl); + } + }; + + /// %Test factor/result aliasing + class ProductModVarFactorResultAlias : public Test { + public: + ProductModVarFactorResultAlias(const std::string& s, + const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::FactorResultAlias::"+ + str(ipl)+"::"+s,2,d,false,ipl) {} + virtual bool solution(const Assignment& x) const { + if (x[1] <= 0) + return false; + int r = x[0] % x[1]; + if (r < 0) r += x[1]; + return r == x[0]; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[0],ipl); + } + }; + + /// %Test modulus/result aliasing, which is necessarily inconsistent + class ProductModVarModResultAlias : public Test { + public: + ProductModVarModResultAlias(const std::string& s, + const Gecode::IntSet& d, + Gecode::IntPropLevel ipl) + : Test("Arithmetic::ProductModVar::ModResultAlias::"+ + str(ipl)+"::"+s,2,d,false,ipl) {} + virtual bool solution(const Assignment&) const { return false; } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[1],ipl); + } + }; + + /// Targeted bounds and arithmetic checks for a variable modulus + class ProductModVarBounds : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + ProductModVarBounds(void) + : ::Test::Base("Int::Arithmetic::ProductModVar::Bounds") {} + virtual bool run(void) { + using namespace Gecode; + { + TestSpace home; + const int vm[5] = {-7,0,1,4,9}; + IntVar m(home,IntSet(vm,5)); + IntVar y(home,-5,12); + product_mod(home,IntVarArgs(),m,y); + if ((home.status() == SS_FAILED) || (m.min() != 1) || + (y.min() != 0) || (y.max() >= m.max())) + return false; + } + { + TestSpace home; + IntVar x(home,Gecode::Int::Limits::max,Gecode::Int::Limits::max); + IntVar m(home,Gecode::Int::Limits::max,Gecode::Int::Limits::max); + IntVar y(home,0,0); + product_mod(home,IntVarArgs({x}),m,y); + if (home.status() == SS_FAILED) + return false; + } + return true; + } + }; + /// %Test that a nonpositive modular-product modulus is rejected class ProductModInvalidModulus : public ::Test::Base { protected: @@ -1547,6 +1708,15 @@ namespace Test { namespace Int { (void) new ProductModSingleton("C",g,5,ipls.ipl()); (void) new ProductModXXYAlias("C",q,5,ipls.ipl()); + (void) new ProductModVarXYMR("C",q,ipls.ipl()); + (void) new ProductModVarXYMR("Sparse",g,ipls.ipl()); + (void) new ProductModVarEmpty("C",q,ipls.ipl()); + (void) new ProductModVarSingleton("C",q,ipls.ipl()); + (void) new ProductModVarRepeated("C",q,ipls.ipl()); + (void) new ProductModVarModFactorAlias("C",q,ipls.ipl()); + (void) new ProductModVarFactorResultAlias("C",q,ipls.ipl()); + (void) new ProductModVarModResultAlias("C",q,ipls.ipl()); + (void) new AbsXY("A",a,ipls.ipl()); (void) new AbsXY("B",b,ipls.ipl()); (void) new AbsXY("C",c,ipls.ipl()); @@ -1697,6 +1867,7 @@ namespace Test { namespace Int { (void) new ArgMinBoolShared(i,false); } (void) new ProductModInvalidModulus; + (void) new ProductModVarBounds; (void) new ProductBoundsLarge; } }; From 6688970d621b80f73104a9c65c912f30a194770e Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 13:22:42 +0200 Subject: [PATCH 08/35] Add reified variable-modulus product propagator Zdev-Change-Id: Z4e982ffb9b706ba0158a82807fcf9de5bf1be32e41238c704e32451054d53743 --- .zd/gcd/TASKS.md | 6 +- ...-ary-product-mod-with-an-intvar-modulus.md | 25 ++- gecode/int.hh | 11 ++ gecode/int/arithmetic.cpp | 23 +++ gecode/int/arithmetic.hh | 23 +++ gecode/int/arithmetic/product-mod.hpp | 149 ++++++++++++++++-- test/int/arithmetic.cpp | 89 ++++++++++- 7 files changed, 298 insertions(+), 28 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index d211bb251c..afc02fbeed 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,9 +3,9 @@ # Tasks: gcd - Total: 7 -- Ready: 1 +- Ready: 0 - Blocked: 0 -- Done: 6 +- Done: 7 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -15,4 +15,4 @@ | [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | | [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | | [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | -| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | ready | gcd-006 | +| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | diff --git a/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md index 6f4b84428e..b996306c00 100644 --- a/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md +++ b/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-007" key = "variable-product-mod-reified" area = "gcd" -status = "open" +status = "done" blocked_by = ["gcd-006"] +++ # Implement reified n-ary product_mod with an IntVar modulus @@ -20,12 +20,12 @@ Users can reify the full positive-variable-modulus product_mod proposition in ev ## Done when -- [ ] A documented public product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, Reify, and IntPropLevel. -- [ ] The reified proposition includes modulus > 0, canonical range, and congruence, with correct RM_EQV, RM_IMP, and RM_PMI behavior. -- [ ] Inactive implications do not narrow the modulus, factors, or result; positive control rewrites to or shares the ordinary variable-modulus propagator. -- [ ] The actor handles fixed and unfixed Boolean controls, nonpositive modulus candidates, sparse domains, modulus one, empty and singleton products, signs, zeros, aliases, cloning, rewriting, and subsumption soundly. -- [ ] Focused existing-harness tests cover all reification modes and fixed/unfixed controls, including inactive implications and false supports from nonpositive moduli or noncanonical results. -- [ ] The affected integer library and focused ProductMod tests build and pass. +- [x] A documented public product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, Reify, and IntPropLevel. +- [x] The reified proposition includes modulus > 0, canonical range, and congruence, with correct RM_EQV, RM_IMP, and RM_PMI behavior. +- [x] Inactive implications do not narrow the modulus, factors, or result; positive control rewrites to or shares the ordinary variable-modulus propagator. +- [x] The actor handles fixed and unfixed Boolean controls, nonpositive modulus candidates, sparse domains, modulus one, empty and singleton products, signs, zeros, aliases, cloning, rewriting, and subsumption soundly. +- [x] Focused existing-harness tests cover all reification modes and fixed/unfixed controls, including inactive implications and false supports from nonpositive moduli or noncanonical results. +- [x] The affected integer library and focused ProductMod tests build and pass. ## Validation @@ -33,3 +33,14 @@ Users can reify the full positive-variable-modulus product_mod proposition in ev - Run the focused reified variable-modulus ProductMod tests with all reification modes. - Run the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Implemented and independently verified reified n-ary product_mod with an IntVar modulus for RM_EQV, RM_IMP, and RM_PMI. + +Validation: + +- Built gecode-test and the affected integer library successfully. +- All 26 ProductModVar cases and all 19 fixed ProductMod regressions passed. +- Broader Int::Arithmetic tests, fresh install, exported API checks, and installed-consumer reification smoke passed. +- git diff --check and zd check gcd passed. diff --git a/gecode/int.hh b/gecode/int.hh index aeb95fb9ab..b969e00727 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3113,6 +3113,17 @@ namespace Gecode { product_mod(Home home, const IntVarArgs& x, IntVar m, IntVar y, IntPropLevel ipl=IPL_DEF); + /** \brief Reify whether \a y is the product of \a x modulo \a m + * + * The reified proposition includes \f$m>0\f$, the canonical range + * \f$0\leq y xv(home,x); + switch (r.mode()) { + case RM_EQV: + GECODE_ES_FAIL((Arithmetic::ReProductModVar + ::post(home,xv,m,y,r.var()))); + break; + case RM_IMP: + GECODE_ES_FAIL((Arithmetic::ReProductModVar + ::post(home,xv,m,y,r.var()))); + break; + case RM_PMI: + GECODE_ES_FAIL((Arithmetic::ReProductModVar + ::post(home,xv,m,y,r.var()))); + break; + default: GECODE_NEVER; + } + } + void divmod(Home home, IntVar x0, IntVar x1, IntVar x2, IntVar x3, diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 79c1e026da..5a0f77b82e 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -941,6 +941,29 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual size_t dispose(Space& home); }; + /** \brief Reified domain propagator for a variable-modulus product + * \ingroup FuncIntProp + */ + template + class ReProductModVar : public Propagator { + protected: + ViewArray x; + IntView m; + IntView y; + BoolView b; + ReProductModVar(Home home, ViewArray& x, IntView m, IntView y, + BoolView b); + ReProductModVar(Space& home, ReProductModVar& p); + public: + static ExecStatus post(Home home, ViewArray& x, IntView m, + IntView y, BoolView b); + virtual Actor* copy(Space& home); + virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + virtual void reschedule(Space& home); + virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + virtual size_t dispose(Space& home); + }; + /** \brief Reified domain propagator for a fixed-modulus n-ary product * \ingroup FuncIntProp */ diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 3245aa39d2..d91849f5f7 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -233,7 +233,9 @@ namespace Gecode { namespace Int { namespace Arithmetic { inline ExecStatus product_mod_var_supports(Space& home, ViewArray& x, - IntView m, IntView y) { + IntView m, IntView y, bool positive=true, + bool filter=true, bool* has_true_out=nullptr, + bool* has_false_out=nullptr) { const int n = x.size(); const int nv = n+2; Region r; @@ -245,7 +247,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { for (int i=0; i(cap64); - int** support = r.alloc(nv); + int** support = filter ? r.alloc(nv) : nullptr; for (int i=0; i vi(x[i]); vi(); ++vi) values[i][k++] = vi.val(); - support[i] = r.alloc(cap); + if (filter) support[i] = r.alloc(cap); } size[n] = m.size(); pos[n] = 0; values[n] = r.alloc(size[n]); unsigned int k=0; for (ViewValues vi(m); vi(); ++vi) values[n][k++] = vi.val(); - support[n] = r.alloc(cap); + if (filter) support[n] = r.alloc(cap); size[n+1] = y.size(); pos[n+1] = 0; values[n+1] = r.alloc(size[n+1]); k=0; for (ViewValues vi(y); vi(); ++vi) values[n+1][k++] = vi.val(); - support[n+1] = r.alloc(cap); + if (filter) support[n+1] = r.alloc(cap); int* tuple = r.alloc(nv); unsigned int ns = 0; + bool has_true = false, has_false = false; bool more = true; while (more) { for (int i=0; i 0) && - (product_mod_value(tuple,n,tuple[n]) == tuple[n+1])) { - for (int i=0; i 0) && (tuple[n+1] >= 0) && + (tuple[n+1] < tuple[n]) && + (product_mod_value(tuple,n,tuple[n]) == tuple[n+1]); + has_true |= relation; has_false |= !relation; + if (filter && (relation == positive)) { + for (int i=0; i= 0) && (++pos[i] == size[i])) { @@ -288,6 +295,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { more = i >= 0; } + if (has_true_out != nullptr) *has_true_out = has_true; + if (has_false_out != nullptr) *has_false_out = has_false; + if (!filter) + return ES_OK; if (ns == 0) return ES_FAILED; for (int i=0; i + forceinline + ReProductModVar::ReProductModVar(Home home, ViewArray& z, + IntView modulus, IntView w, BoolView c) + : Propagator(home), x(z), m(modulus), y(w), b(c) { + x.subscribe(home,*this,PC_INT_DOM); + m.subscribe(home,*this,PC_INT_DOM); + y.subscribe(home,*this,PC_INT_DOM); + b.subscribe(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProductModVar::post(Home home, ViewArray& x, IntView m, + IntView y, BoolView b) { + if (b.one()) { + if (rm == RM_PMI) return ES_OK; + return ProductModVar::post(home,x,m,y); + } + if (b.zero() && (rm == RM_IMP)) + return ES_OK; + (void) new (home) ReProductModVar(home,x,m,y,b); + return ES_OK; + } + + template + forceinline + ReProductModVar::ReProductModVar(Space& home, + ReProductModVar& p) + : Propagator(home,p) { + x.update(home,p.x); m.update(home,p.m); y.update(home,p.y); + b.update(home,p.b); + } + + template + forceinline Actor* + ReProductModVar::copy(Space& home) { + return new (home) ReProductModVar(home,*this); + } + + template + forceinline PropCost + ReProductModVar::cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,x.size()+3); + } + + template + forceinline void + ReProductModVar::reschedule(Space& home) { + x.reschedule(home,*this,PC_INT_DOM); + m.reschedule(home,*this,PC_INT_DOM); + y.reschedule(home,*this,PC_INT_DOM); + b.reschedule(home,*this,PC_BOOL_VAL); + } + + template + inline ExecStatus + ReProductModVar::propagate(Space& home, const ModEventDelta&) { + if (b.one()) { + if (rm == RM_PMI) + return home.ES_SUBSUMED(*this); + GECODE_REWRITE(*this,ProductModVar::post(home(*this),x,m,y)); + } + if (b.zero()) { + if (rm == RM_IMP) + return home.ES_SUBSUMED(*this); + if (product_mod_var_enumerable(x,m,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_mod_var_supports + (home,x,m,y,false,true,&ht,&hf)); + if (!ht) + return home.ES_SUBSUMED(*this); + } else if ((m == y) || (m.max() <= 0) || (y.max() < 0) || + (y.min() >= m.max())) { + return home.ES_SUBSUMED(*this); + } + return ES_FIX; + } + + RelTest rt; + if (m == y) { + rt = RT_FALSE; + } else if ((m.max() <= 0) || (y.max() < 0) || (y.min() >= m.max())) { + rt = RT_FALSE; + } else if (product_mod_var_enumerable(x,m,y)) { + bool ht, hf; + GECODE_ES_CHECK(product_mod_var_supports + (home,x,m,y,true,false,&ht,&hf)); + rt = !ht ? RT_FALSE : (!hf ? RT_TRUE : RT_MAYBE); + } else { + rt = RT_MAYBE; + } + switch (rt) { + case RT_TRUE: + if (rm != RM_IMP) GECODE_ME_CHECK(b.one_none(home)); + return home.ES_SUBSUMED(*this); + case RT_FALSE: + if (rm != RM_PMI) GECODE_ME_CHECK(b.zero_none(home)); + return home.ES_SUBSUMED(*this); + case RT_MAYBE: + return ES_FIX; + default: GECODE_NEVER; + } + } + + template + forceinline size_t + ReProductModVar::dispose(Space& home) { + x.cancel(home,*this,PC_INT_DOM); + m.cancel(home,*this,PC_INT_DOM); + y.cancel(home,*this,PC_INT_DOM); + b.cancel(home,*this,PC_BOOL_VAL); + (void) Propagator::dispose(home); + return sizeof(*this); + } + template forceinline ReProductMod::ReProductMod(Home home, ViewArray& z, diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index f12851ceba..db95153c46 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -446,7 +446,7 @@ namespace Test { namespace Int { ProductModVarXYMR(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::XYMR::"+str(ipl)+"::"+s, - 4,d,false,ipl) {} + 4,d,true,ipl) {} virtual bool solution(const Assignment& x) const { return (x[2] > 0) && (product_mod_value(x,2,x[2]) == x[3]); @@ -455,6 +455,11 @@ namespace Test { namespace Int { Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[1]}), x[2],x[3],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[1]}), + x[2],x[3],r,ipl); + } }; /// %Test for the empty product with a variable modulus @@ -463,13 +468,17 @@ namespace Test { namespace Int { ProductModVarEmpty(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Empty::"+str(ipl)+"::"+s, - 2,d,false,ipl) {} + 2,d,true,ipl) {} virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == (1 % x[0])); } virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { Gecode::product_mod(home,Gecode::IntVarArgs(),x[0],x[1],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs(),x[0],x[1],r,ipl); + } }; /// %Test for a singleton product with a variable modulus @@ -478,7 +487,7 @@ namespace Test { namespace Int { ProductModVarSingleton(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Singleton::"+str(ipl)+"::"+s, - 3,d,false,ipl) {} + 3,d,true,ipl) {} virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -491,6 +500,11 @@ namespace Test { namespace Int { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), x[1],x[2],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[2],r,ipl); + } }; /// %Test repeated factors with a variable modulus @@ -499,7 +513,7 @@ namespace Test { namespace Int { ProductModVarRepeated(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Repeated::"+str(ipl)+"::"+s, - 3,d,false,ipl) {} + 3,d,true,ipl) {} virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -511,6 +525,11 @@ namespace Test { namespace Int { Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[0]}), x[1],x[2],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0],x[0]}), + x[1],x[2],r,ipl); + } }; /// %Test modulus/factor aliasing @@ -520,7 +539,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModFactorAlias::"+ - str(ipl)+"::"+s,2,d,false,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) {} virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == 0); } @@ -528,6 +547,11 @@ namespace Test { namespace Int { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), x[0],x[1],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[0],x[1],r,ipl); + } }; /// %Test factor/result aliasing @@ -537,7 +561,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::FactorResultAlias::"+ - str(ipl)+"::"+s,2,d,false,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) {} virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -549,6 +573,11 @@ namespace Test { namespace Int { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), x[1],x[0],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[0],r,ipl); + } }; /// %Test modulus/result aliasing, which is necessarily inconsistent @@ -558,12 +587,17 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModResultAlias::"+ - str(ipl)+"::"+s,2,d,false,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) {} virtual bool solution(const Assignment&) const { return false; } virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), x[1],x[1],ipl); } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) { + Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), + x[1],x[1],r,ipl); + } }; /// Targeted bounds and arithmetic checks for a variable modulus @@ -601,6 +635,46 @@ namespace Test { namespace Int { } }; + /// Verify that inactive implication modes leave all integer views alone + class ProductModVarInactive : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + static bool unchanged(const Gecode::IntVar& x, const int* v, int n) { + if (x.size() != static_cast(n)) + return false; + for (int i=0; i Date: Wed, 12 Aug 2026 13:38:13 +0200 Subject: [PATCH 09/35] Plan multiplication special-case propagation Zdev-Change-Id: Zf5f690820adf5b0c9bb6738861c2e96a68dc91647e0295935dc589bd35079a16 --- .zd/gcd/TASKS.md | 11 ++++-- .zd/gcd/brief.md | 24 ++++++++++++ ...-aware-inverse-bounds-for-n-ary-product.md | 36 ++++++++++++++++++ ...nit-and-sign-rewrites-for-n-ary-product.md | 36 ++++++++++++++++++ ...nd-result-alias-rewrites-for-n-ary-prod.md | 36 ++++++++++++++++++ ...ll-modulus-propagation-for-fixed-produc.md | 36 ++++++++++++++++++ ...ic-propagation-for-variable-product-mod.md | 37 +++++++++++++++++++ 7 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 .zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md create mode 100644 .zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md create mode 100644 .zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md create mode 100644 .zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md create mode 100644 .zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index afc02fbeed..cd51580bdc 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -2,9 +2,9 @@ # Tasks: gcd -- Total: 7 -- Ready: 0 -- Blocked: 0 +- Total: 12 +- Ready: 4 +- Blocked: 1 - Done: 7 | ID | Task | State | Blocked by | @@ -16,3 +16,8 @@ | [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | | [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | | [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | +| [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | ready | — | +| [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | +| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | +| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and small-modulus propagation for fixed product_mod | ready | — | +| [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | ready | — | diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index e2e6e35480..820f8e3175 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -29,6 +29,13 @@ public APIs, and focused regression coverage. empty, repeated, and aliased variable domains as applicable; their actors clone, subscribe, reschedule, rewrite, and subsume according to existing Gecode conventions. +- Exact product propagation exploits strict and non-strict sign classes, + zero-aware inverse bounds, assigned units, repeated factors, powers, and + result aliases rather than treating every factor occurrence independently. +- Product-mod propagation exploits algebraic cases that remain useful beyond + capped Cartesian enumeration, including zero factors, factors equal to the + modulus, small fixed moduli, empty products, and divisor reasoning for an + assigned exact product or product-result difference. - Focused integer tests validate solutions and propagation through the existing test harness. @@ -44,6 +51,9 @@ public APIs, and focused regression coverage. not required. - Variable-modulus `product_mod` accepts one `IntVar` modulus; MiniModel and FlatZinc exposure remain outside scope. +- Gecode has no proof-logging subsystem. Proof encodings, proof certificates, + and proof-logging APIs are outside scope; multiplication proof-logging work + may motivate mathematical case analysis but not implementation artifacts. ## Terms @@ -61,6 +71,9 @@ public APIs, and focused regression coverage. `0 <= r < m`. Thus `product_mod([],m) = 1 mod m`, including zero when `m=1`. - Reification follows Gecode's `Reify` modes: equivalence (`RM_EQV`), forward implication (`RM_IMP`), and reverse implication (`RM_PMI`). +- Multiplication sign classes distinguish zero, positive, nonnegative, + negative, nonpositive, and mixed domains. Strict sign classes exclude zero; + non-strict classes retain zero as a semantically important case. ## Decisions @@ -82,6 +95,13 @@ public APIs, and focused regression coverage. - For a variable modulus, positivity is part of the `product_mod` relation. The ordinary constraint enforces it. Reification controls the full proposition, so inactive implications do not narrow the modulus or result. +- Exact-product inverse propagation follows the complete zero-aware quotient + cases used by Gecode-style multiplication: no filtering when cofactor and + result both contain zero, failure for a zero cofactor with a nonzero result, + magnitude filtering when the cofactor spans zero but the result does not, + and directed floor/ceiling division for zero-free or one-sided cofactors. +- Product-mod specialisation uses modular algebra rather than ordinary-product + sign monotonicity, which does not survive Euclidean reduction. ## Open questions @@ -101,6 +121,10 @@ aliasing, exact-product overflow boundaries, and all reification modes. For and singleton arrays, aliasing, and all reification modes. Variable-modulus coverage also includes nonpositive candidates, sparse modulus domains, modulus/result or modulus/factor aliasing, and inactive implication behavior. +Focused propagation cases additionally cover all strict and non-strict sign +classes, zero-containing cofactors, assigned zero/one/minus-one factors, +repeated factors, result aliases, small-modulus residue sets, and large-domain +algebraic pruning that does not depend on Cartesian enumeration. Reuse existing arithmetic test helpers and commands; do not create new test infrastructure or an exhaustive large-domain performance matrix. diff --git a/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md b/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md new file mode 100644 index 0000000000..cdfdfdbb1f --- /dev/null +++ b/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md @@ -0,0 +1,36 @@ ++++ +schema_version = 1 +id = "gcd-008" +key = "product-zero-aware-inverse-bounds" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Complete zero-aware inverse bounds for n-ary product + +## Outcome + +The exact n-ary Product actor performs sound backward bounds propagation for positive, nonnegative, negative, nonpositive, mixed, and zero cofactor domains. + +## Boundaries + +- Keep the exact-product public APIs and mathematical semantics unchanged. +- Do not add proof logging, proof encodings, or proof-only state. +- Do not change product_mod in this task. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [ ] Backward factor propagation implements the full zero-aware quotient case split: no filter when cofactor and result both contain zero, failure when a zero cofactor cannot produce the result, magnitude bounds when a cofactor spans zero and the result excludes zero, and directed floor/ceiling quotient bounds for one-sided cofactors. +- [ ] Positive, nonnegative, negative, nonpositive, mixed, and fixed-zero domain combinations produce sound bounds without division by zero or signed overflow. +- [ ] Cofactor interval computation is structured to avoid unnecessary repeated whole-array recomputation while preserving safe saturation and alias behavior. +- [ ] Ordinary propagation and positive reified rewrites reach an honest fixpoint with correct subscriptions, costs, cloning, and subsumption. +- [ ] Focused tests exercise every sign/zero case, large domains, aliases, and overflow boundaries through the existing Product harness. +- [ ] The affected integer library and focused Product tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run focused Int::Arithmetic::Product sign-class and inverse-bound tests with multiple iterations. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. diff --git a/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md b/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md new file mode 100644 index 0000000000..30271de9a3 --- /dev/null +++ b/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md @@ -0,0 +1,36 @@ ++++ +schema_version = 1 +id = "gcd-009" +key = "product-zero-unit-sign-rewrites" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Add zero, unit, and sign rewrites for n-ary product + +## Outcome + +Exact product posting and propagation simplify assigned zero, one, and minus-one factors and exploit stable aggregate sign information. + +## Boundaries + +- Keep repeated-variable powers and result-alias identities for the dependent alias-rewrites task. +- Do not add proof logging or change product_mod. +- Preserve all ordinary and reified API semantics, including inactive implications. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [ ] A fixed-zero factor assigns the positive relation's result to zero; assigned one factors are removed; assigned minus-one factors are removed with the result sign transformed safely. +- [ ] If the result excludes zero, bounds-visible zero endpoints are removed from nonnegative or nonpositive factors, and a zero result with exactly one possible zero source propagates that source to zero when sound. +- [ ] Aggregate sign parity and zero possibility constrain the result sign and permit safe normalisation or rewriting to a positive-magnitude path when all remaining factors have strict signs. +- [ ] Reified actors apply these deductions only when truth is required and preserve no-narrowing inactive implication behavior. +- [ ] Focused tests cover zero, one, minus one, strict and non-strict sign combinations, sign parity, reification controls, cloning, and representable limits. +- [ ] The affected integer library and focused Product tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run focused Int::Arithmetic::Product simplification and sign tests with multiple iterations. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. diff --git a/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md b/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md new file mode 100644 index 0000000000..4323fabe98 --- /dev/null +++ b/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md @@ -0,0 +1,36 @@ ++++ +schema_version = 1 +id = "gcd-010" +key = "product-alias-power-rewrites" +area = "gcd" +status = "open" +blocked_by = ["gcd-009"] ++++ +# Add repeated-factor and result-alias rewrites for n-ary product + +## Outcome + +Exact product propagation recognises repeated variables as powers and result aliases as algebraic identities instead of independent interval occurrences. + +## Boundaries + +- Build on the zero, unit, and sign simplification machinery from the blocking task. +- Do not add proof logging or change product_mod. +- Use existing Gecode power, square, view, and actor patterns where they preserve the direct n-ary overflow and zero semantics. +- Follow the brief's focused testing level and existing arithmetic-test patterns. + +## Done when + +- [ ] Repeated occurrences of the same factor exploit even/odd power structure, including a nonnegative lower bound for even powers over mixed domains and sound inverse power bounds. +- [ ] The implementation does not use an intermediate-product decomposition that rejects a valid final zero merely because an earlier partial product is nonrepresentable. +- [ ] A result variable appearing among the factors is simplified using the identity result=0 or product(other factors)=1, with correct handling of multiple occurrences. +- [ ] Aliased and repeated views remain sound under cloning, propagation, reification, overflow, and zero cases. +- [ ] Focused tests demonstrate stronger pruning for squares/even powers, odd powers, repeated factors, single and multiple result aliases, signs, zeros, and reification modes. +- [ ] The affected integer library and focused Product tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run focused Int::Arithmetic::Product alias and power tests with multiple iterations. +- Run the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. diff --git a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md new file mode 100644 index 0000000000..c9c355c45b --- /dev/null +++ b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md @@ -0,0 +1,36 @@ ++++ +schema_version = 1 +id = "gcd-011" +key = "product-mod-fixed-specialisation" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Add algebraic and small-modulus propagation for fixed product_mod + +## Outcome + +Fixed-modulus product_mod propagates important algebraic cases and small reachable residue sets without relying on full Cartesian tuple enumeration. + +## Boundaries + +- Keep the fixed positive int modulus API and Euclidean residue semantics unchanged. +- Do not add proof logging or modify variable-modulus APIs except shared helpers that preserve behavior. +- Use bounded residue-set propagation only where its memory and runtime are controlled by an explicit small-modulus policy. +- Preserve all reification modes and inactive implication behavior. + +## Done when + +- [ ] Modulus one, a fixed-zero factor, and a factor fixed to a multiple of the modulus force result zero; empty and assigned unit factors simplify directly. +- [ ] When exact-product bounds fit one quotient band, the relation rewrites or propagates as result=product-k*modulus, including the no-wrap case. +- [ ] For explicitly bounded small fixed moduli, reachable residues are propagated incrementally across factors without enumerating the full Cartesian product and project sound supports to the result and factors where maintained. +- [ ] Large-modulus or unsuitable domains fall back to the existing sound path without unbounded allocation or work. +- [ ] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, small residue sets, controls, cloning, and large-domain fallback. +- [ ] The affected integer library and focused ProductMod tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run focused fixed-modulus Int::Arithmetic::ProductMod tests with multiple iterations. +- Run variable-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. diff --git a/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md b/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md new file mode 100644 index 0000000000..0c4967f0b7 --- /dev/null +++ b/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md @@ -0,0 +1,37 @@ ++++ +schema_version = 1 +id = "gcd-012" +key = "product-mod-variable-specialisation" +area = "gcd" +status = "open" +blocked_by = [] ++++ +# Add algebraic propagation for variable product_mod + +## Outcome + +Variable-modulus product_mod derives useful modulus and result information from identities and assigned products even when Cartesian support enumeration is capped. + +## Boundaries + +- Keep positivity as part of the full reified proposition and preserve inactive implication no-narrowing behavior. +- Do not add proof logging or change fixed-modulus semantics. +- Do not compute an overflowing exact product; use safe zero detection, checked arithmetic, or divisor reasoning only when the required difference is representable. +- Follow the brief's focused testing level and existing ProductModVar patterns. + +## Done when + +- [ ] A factor aliased with the modulus forces canonical result zero in the positive relation, while modulus=result remains correctly impossible. +- [ ] The empty product propagates the piecewise identity m=1 implies result=0 and m>=2 implies result=1, including backward pruning from the result. +- [ ] For assigned factors and result, modulus candidates are filtered by m>result and divisibility of product-result; product=result preserves every modulus above result. +- [ ] A zero result exposes sound modulus-divides-product reasoning where the product is safely known, without requiring Cartesian enumeration. +- [ ] Reified false and unfixed paths treat invalid modulus/result combinations correctly and leave inactive implications untouched. +- [ ] Focused tests cover empty products, assigned product/difference divisors, factor-modulus aliases, zero result, sparse and large modulus domains, all reification modes, cloning, and arithmetic limits. +- [ ] The affected integer library and focused ProductModVar tests build and pass. + +## Validation + +- Build the configured integer library and gecode-test executable. +- Run focused Int::Arithmetic::ProductModVar algebraic tests with multiple iterations. +- Run fixed-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. +- Run git diff --check and zd check gcd --format json. From 838e3fd6c389508e796405503561e95241a60110 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 13:43:58 +0200 Subject: [PATCH 10/35] Plan removal of support enumeration Zdev-Change-Id: Z1413d81cb9a8151dc29dfd102a58b9ad5ba4817840c63b4723cbd699b6700919 --- .zd/gcd/TASKS.md | 7 ++-- .zd/gcd/brief.md | 12 +++++- ...ll-modulus-propagation-for-fixed-produc.md | 12 +++--- ...meration-from-number-theoretic-propagat.md | 37 +++++++++++++++++++ 4 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 .zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index cd51580bdc..a5b6a91056 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -2,9 +2,9 @@ # Tasks: gcd -- Total: 12 +- Total: 13 - Ready: 4 -- Blocked: 1 +- Blocked: 2 - Done: 7 | ID | Task | State | Blocked by | @@ -19,5 +19,6 @@ | [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | ready | — | | [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | | [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | -| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and small-modulus propagation for fixed product_mod | ready | — | +| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | ready | — | | [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | ready | — | +| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | blocked | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index 820f8e3175..7c141f9b32 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -102,6 +102,12 @@ public APIs, and focused regression coverage. and directed floor/ceiling division for zero-free or one-sided cofactors. - Product-mod specialisation uses modular algebra rather than ordinary-product sign monotonicity, which does not survive Euclidean reduction. +- None of the propagators introduced in this area use Cartesian domain-tuple + enumeration, support tables, or projected tuple supports as a propagation + strategy. Prefer bounds, sign and zero classification, divisibility, + congruence, and algebraic rewrites. A fully assigned relation may be + evaluated directly; reified relations otherwise remain conservative when + entailment or disentailment cannot be established algebraically. ## Open questions @@ -123,8 +129,10 @@ coverage also includes nonpositive candidates, sparse modulus domains, modulus/result or modulus/factor aliasing, and inactive implication behavior. Focused propagation cases additionally cover all strict and non-strict sign classes, zero-containing cofactors, assigned zero/one/minus-one factors, -repeated factors, result aliases, small-modulus residue sets, and large-domain -algebraic pruning that does not depend on Cartesian enumeration. +repeated factors, result aliases, and large-domain algebraic pruning that does +not depend on Cartesian enumeration or support projection. Include cases above +the former 200,000-tuple cutoff so useful behavior and fully assigned checking +cannot accidentally depend on that threshold. Reuse existing arithmetic test helpers and commands; do not create new test infrastructure or an exhaustive large-domain performance matrix. diff --git a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md index c9c355c45b..2e58394625 100644 --- a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md +++ b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md @@ -6,26 +6,26 @@ area = "gcd" status = "open" blocked_by = [] +++ -# Add algebraic and small-modulus propagation for fixed product_mod +# Add algebraic and bounds propagation for fixed product_mod ## Outcome -Fixed-modulus product_mod propagates important algebraic cases and small reachable residue sets without relying on full Cartesian tuple enumeration. +Fixed-modulus product_mod propagates important algebraic, interval, and congruence cases without enumerating domain tuples or projected supports. ## Boundaries - Keep the fixed positive int modulus API and Euclidean residue semantics unchanged. - Do not add proof logging or modify variable-modulus APIs except shared helpers that preserve behavior. -- Use bounded residue-set propagation only where its memory and runtime are controlled by an explicit small-modulus policy. +- Use bounds, quotient-band, divisibility, and congruence reasoning; do not build reachable-residue support tables or enumerate factor/result combinations. - Preserve all reification modes and inactive implication behavior. ## Done when - [ ] Modulus one, a fixed-zero factor, and a factor fixed to a multiple of the modulus force result zero; empty and assigned unit factors simplify directly. - [ ] When exact-product bounds fit one quotient band, the relation rewrites or propagates as result=product-k*modulus, including the no-wrap case. -- [ ] For explicitly bounded small fixed moduli, reachable residues are propagated incrementally across factors without enumerating the full Cartesian product and project sound supports to the result and factors where maintained. -- [ ] Large-modulus or unsuitable domains fall back to the existing sound path without unbounded allocation or work. -- [ ] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, small residue sets, controls, cloning, and large-domain fallback. +- [ ] Fixed-modulus reasoning derives sound canonical-result and factor bounds from interval, quotient-band, divisibility, and congruence properties without constructing residue supports. +- [ ] Domains for which those deductions are inconclusive retain a sound conservative path without domain-product thresholds, support allocation, or tuple enumeration. +- [ ] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, controls, cloning, and large-domain behavior. - [ ] The affected integer library and focused ProductMod tests build and pass. ## Validation diff --git a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md new file mode 100644 index 0000000000..1627880a76 --- /dev/null +++ b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md @@ -0,0 +1,37 @@ ++++ +schema_version = 1 +id = "gcd-013" +key = "eliminate-support-enumeration" +area = "gcd" +status = "open" +blocked_by = ["gcd-008", "gcd-009", "gcd-010", "gcd-011", "gcd-012"] ++++ +# Eliminate support enumeration from number-theoretic propagators + +## Outcome + +Every propagator added in the gcd area uses bounds or algebraic reasoning instead of Cartesian support enumeration for propagation and reification status. + +## Boundaries + +- Audit Gcd/ReGcd, ReDivides, Product/ReProduct, ProductMod/ReProductMod, and ProductModVar/ReProductModVar, including shared helpers. +- Do not change the public relation semantics or add proof-logging artifacts. +- Direct evaluation is allowed only when all variables relevant to the relation are assigned; do not collect or project tuple supports. +- Use the existing integer arithmetic test harness and the focused testing level in the area brief. + +## Done when + +- [ ] No audited propagator or helper enumerates a Cartesian product of variable domains, allocates tuple-support tables, or filters domains by projected tuple supports. +- [ ] Ordinary propagation is expressed through sound bounds, sign/zero classification, divisibility, congruence, or algebraic rewrites, with conservative behavior where those deductions are inconclusive. +- [ ] Reified entailment and disentailment use sound bounds or algebraic tests and exact evaluation only for fully assigned relations; negative or unfixed cases wait conservatively instead of enumerating supports. +- [ ] Propagation conditions, subscriptions, and costs match the information actually used, including bounds subscriptions where interior-domain events are no longer required. +- [ ] Focused tests exercise GCD, divides, product, fixed-modulus product_mod, and variable-modulus product_mod on domains above the former 200,000-tuple cutoff, relevant reification modes, and fully assigned true and false leaves. +- [ ] The affected integer library, all focused new-propagator tests with multiple iterations, and the broader integer arithmetic suite build and pass. + +## Validation + +- Search the gcd-area implementation for support-table, Cartesian-enumeration, and tuple-projection machinery and inspect every remaining domain iterator. +- Build the configured integer library and gecode-test executable. +- Run focused Gcd, Divides, Product, ProductMod, and ProductModVar tests with multiple iterations. +- Run the broader Int::Arithmetic tests. +- Run git diff --check and zd check gcd --format json. From f6f821d2d1c9383d96cb258c0eb3c8111f064224 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 14:08:59 +0200 Subject: [PATCH 11/35] Strengthen fixed product-mod propagation Zdev-Change-Id: Z6be39616f11def89cb85d64bbb8d2fe06bef1d1e81a13746fff2eda9c3ae03fe --- .zd/gcd/TASKS.md | 6 +- ...ll-modulus-propagation-for-fixed-produc.md | 24 +- gecode/int/arithmetic/product-mod.hpp | 378 ++++++++++++------ test/int/arithmetic.cpp | 118 +++++- 4 files changed, 382 insertions(+), 144 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index a5b6a91056..30cabc5cca 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,9 +3,9 @@ # Tasks: gcd - Total: 13 -- Ready: 4 +- Ready: 3 - Blocked: 2 -- Done: 7 +- Done: 8 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -19,6 +19,6 @@ | [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | ready | — | | [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | | [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | -| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | ready | — | +| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | | [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | ready | — | | [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | blocked | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md index 2e58394625..825dcf7f70 100644 --- a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md +++ b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-011" key = "product-mod-fixed-specialisation" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Add algebraic and bounds propagation for fixed product_mod @@ -21,12 +21,12 @@ Fixed-modulus product_mod propagates important algebraic, interval, and congruen ## Done when -- [ ] Modulus one, a fixed-zero factor, and a factor fixed to a multiple of the modulus force result zero; empty and assigned unit factors simplify directly. -- [ ] When exact-product bounds fit one quotient band, the relation rewrites or propagates as result=product-k*modulus, including the no-wrap case. -- [ ] Fixed-modulus reasoning derives sound canonical-result and factor bounds from interval, quotient-band, divisibility, and congruence properties without constructing residue supports. -- [ ] Domains for which those deductions are inconclusive retain a sound conservative path without domain-product thresholds, support allocation, or tuple enumeration. -- [ ] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, controls, cloning, and large-domain behavior. -- [ ] The affected integer library and focused ProductMod tests build and pass. +- [x] Modulus one, a fixed-zero factor, and a factor fixed to a multiple of the modulus force result zero; empty and assigned unit factors simplify directly. +- [x] When exact-product bounds fit one quotient band, the relation rewrites or propagates as result=product-k*modulus, including the no-wrap case. +- [x] Fixed-modulus reasoning derives sound canonical-result and factor bounds from interval, quotient-band, divisibility, and congruence properties without constructing residue supports. +- [x] Domains for which those deductions are inconclusive retain a sound conservative path without domain-product thresholds, support allocation, or tuple enumeration. +- [x] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, controls, cloning, and large-domain behavior. +- [x] The affected integer library and focused ProductMod tests build and pass. ## Validation @@ -34,3 +34,13 @@ Fixed-modulus product_mod propagates important algebraic, interval, and congruen - Run focused fixed-modulus Int::Arithmetic::ProductMod tests with multiple iterations. - Run variable-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Replaced fixed-modulus Cartesian support filtering with threshold-free algebraic bounds, quotient-band, inverse-bound, zero/unit, and congruence propagation, with sound reified status handling. + +Validation: + +- Built gecodeint_shared and gecode-test successfully. +- Fixed ProductMod tests passed for five iterations; ProductModVar regression and broader Int::Arithmetic passed. +- Independent spec and standards verification passed after correcting ProductMod scheduling cost; git diff --check and zd check passed. diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index d91849f5f7..8874bf140f 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -34,20 +34,6 @@ namespace Gecode { namespace Int { namespace Arithmetic { - const unsigned long long product_mod_support_limit = 200000ULL; - - inline bool - product_mod_enumerable(const ViewArray& x, const IntView& y) { - unsigned long long n = y.size(); - for (int i=0; i product_mod_support_limit / s)) - return false; - n *= s; - } - return n <= product_mod_support_limit; - } - /// Compute the canonical residue without signed overflow. inline int product_mod_value(const int* v, int n, int m) { @@ -62,93 +48,101 @@ namespace Gecode { namespace Int { namespace Arithmetic { return static_cast(p); } + /// Return exact representable product bounds, or false on saturation. inline bool - product_mod_alias_ok(const ViewArray& x, const IntView& y, - const int* v) { - for (int i=0; i& x, ProductInterval& p, + int omit=-1) { + p.min=1; p.max=1; + for (int i=0; i(p.min) * x[i].min(), + static_cast(p.min) * x[i].max(), + static_cast(p.max) * x[i].min(), + static_cast(p.max) * x[i].max() + }; + const long long int l = *std::min_element(q,q+4); + const long long int u = *std::max_element(q,q+4); + if (!Limits::valid(l) || !Limits::valid(u)) return false; - if ((x[i] == y) && (v[i] != v[x.size()])) - return false; - } + p.min=static_cast(l); p.max=static_cast(u); + } return true; } - inline ExecStatus - product_mod_supports(Space& home, ViewArray& x, IntView y, int m, - bool positive, bool filter, - bool& has_true, bool& has_false) { - const int n = x.size(); - Region r; - unsigned int* size = r.alloc(n+1); - unsigned int* pos = r.alloc(n+1); - int** values = r.alloc(n+1); - unsigned long long cap64 = y.size(); - for (int i=0; i(cap64); - int** support = filter ? r.alloc(n+1) : nullptr; + /// Return whether an assigned factor makes the residue identically zero. + inline bool + product_mod_zero(const ViewArray& x, int m) { + for (int i=0; i(size[i]); - unsigned int k=0; - for (ViewValues vi(x[i]); vi(); ++vi) - values[i][k++] = vi.val(); - if (filter) - support[i] = r.alloc(cap); + /// Return whether all factors are assigned and compute their residue. + inline bool + product_mod_assigned(const ViewArray& x, int m, int& residue) { + Region r; + int* v = r.alloc(x.size()); + for (int i=0; i(size[n]); - unsigned int k=0; - for (ViewValues vi(y); vi(); ++vi) - values[n][k++] = vi.val(); - if (filter) - support[n] = r.alloc(cap); + residue=product_mod_value(v,x.size(),m); + return true; + } - int* tuple = r.alloc(n+1); - unsigned int ns = 0; - has_true = has_false = false; - bool more = true; - while (more) { - for (int i=0; i<=n; i++) - tuple[i] = values[i][pos[i]]; - if (product_mod_alias_ok(x,y,tuple)) { - const bool relation = - product_mod_value(tuple,n,m) == tuple[n]; - has_true |= relation; has_false |= !relation; - if (filter && (relation == positive)) { - for (int i=0; i<=n; i++) - support[i][ns] = tuple[i]; - ns++; - } - } - int i=n; - while ((i >= 0) && (++pos[i] == size[i])) { - pos[i]=0; i--; + /// Determine the fixed-modulus relation algebraically. + inline RelTest + product_mod_status(const ViewArray& x, const IntView& y, int m) { + if ((y.max() < 0) || (y.min() >= m)) + return RT_FALSE; + const bool zero = (m == 1) || product_mod_zero(x,m); + if (zero) { + if (!y.in(0)) return RT_FALSE; + return y.assigned() ? RT_TRUE : RT_MAYBE; + } + int residue; + if (product_mod_assigned(x,m,residue)) { + if (!y.in(residue)) return RT_FALSE; + return y.assigned() ? RT_TRUE : RT_MAYBE; + } + ProductInterval p; + if (product_mod_interval(x,p)) { + const long long int kl = floor_div_xx + (static_cast(p.min),static_cast(m)); + const long long int ku = floor_div_xx + (static_cast(p.max),static_cast(m)); + if (kl == ku) { + const long long int shift=kl*m; + if ((static_cast(p.max)-shift < y.min()) || + (static_cast(p.min)-shift > y.max())) + return RT_FALSE; } - more = i >= 0; } + return RT_MAYBE; + } - if (!filter) - return ES_OK; - if (ns == 0) - return ES_FAILED; - for (int i=0; i<=n; i++) { - std::sort(support[i],support[i]+ns); - unsigned int nu=1; - for (unsigned int j=1; j& x, int m, IntView y) { GECODE_ME_CHECK(y.gq(home,0)); GECODE_ME_CHECK(y.lq(home,m-1)); + if (m == 1) { + GECODE_ME_CHECK(y.eq(home,0)); + return ES_OK; + } if (x.size() == 0) { GECODE_ME_CHECK(y.eq(home,1 % m)); return ES_OK; } + if (product_mod_zero(x,m)) { + GECODE_ME_CHECK(y.eq(home,0)); + return ES_OK; + } + int residue; + if (product_mod_assigned(x,m,residue)) { + GECODE_ME_CHECK(y.eq(home,residue)); + return ES_OK; + } (void) new (home) ProductMod(home,x,m,y); return ES_OK; } @@ -179,36 +186,157 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline PropCost ProductMod::cost(const Space&, const ModEventDelta&) const { - return PropCost::linear(PropCost::HI,x.size()+1); + return PropCost::quadratic(PropCost::LO,x.size()+1); } inline ExecStatus ProductMod::propagate(Space& home, const ModEventDelta&) { - if (product_mod_enumerable(x,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_mod_supports(home,x,y,m,true,true,ht,hf)); + GECODE_ME_CHECK(y.gq(home,0)); + GECODE_ME_CHECK(y.lq(home,m-1)); + + if (product_mod_zero(x,m)) { + GECODE_ME_CHECK(y.eq(home,0)); + return home.ES_SUBSUMED(*this); } - bool assigned = y.assigned(); - for (int i=0; assigned && (i(p.min),static_cast(m)); + const long long int ku = floor_div_xx + (static_cast(p.max),static_cast(m)); + if (kl == ku) { + const long long int shift=kl*m; + { + const long long int l=static_cast(p.min)-shift; + ModEvent me=y.gq(home,static_cast(l)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + const long long int u=static_cast(p.max)-shift; + ModEvent me=y.lq(home,static_cast(u)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + + // Within one quotient band, invert the exact-product interval. + const long long int tmin=static_cast(y.min())+shift; + const long long int tmax=static_cast(y.max())+shift; + for (int i=0; i 0) || (q.max < 0))) { + long long int c[4] = { + ceil_div_xx(tmin,static_cast(q.min)), + ceil_div_xx(tmin,static_cast(q.max)), + ceil_div_xx(tmax,static_cast(q.min)), + ceil_div_xx(tmax,static_cast(q.max)) + }; + long long int f[4] = { + floor_div_xx(tmin,static_cast(q.min)), + floor_div_xx(tmin,static_cast(q.max)), + floor_div_xx(tmax,static_cast(q.min)), + floor_div_xx(tmax,static_cast(q.max)) + }; + long long int l=*std::min_element(c,c+4); + long long int u=*std::max_element(f,f+4); + l=std::max(l,static_cast(Limits::min)); + u=std::min(u,static_cast(Limits::max)); + if (l > u) return ES_FAILED; + { + ModEvent me=x[i].gq(home,static_cast(l)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me=x[i].lq(home,static_cast(u)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + } + } + } + } + } while (modified); + + // If only one factor is free and the result is assigned, solve the + // resulting linear congruence and tighten to its extreme solutions. + bool congruence_modified=false; + if (y.assigned()) { + int free=-1; + long long int cofactor=1 % m; + for (int i=0; i(x[i].val()) % m; + if (r < 0) r += m; + cofactor=(cofactor*r) % m; + } else if (free < 0) { + free=i; + } else { + free=-2; break; + } + } + if (free >= 0) { + const long long int g=product_mod_gcd(cofactor,m); + if ((y.val() % g) != 0) + return ES_FAILED; + const long long int step=m/g; + long long int r=0; + if (step > 1) { + const long long int a=cofactor/g; + const long long int b=y.val()/g; + r=(product_mod_inverse(a % step,step) * b) % step; + if (r < 0) r += step; + } + const long long int l=r + ceil_div_xx + (static_cast(x[free].min())-r,step)*step; + const long long int u=r + floor_div_xx + (static_cast(x[free].max())-r,step)*step; + if (l > u) return ES_FAILED; + { + ModEvent me=x[free].gq(home,static_cast(l)); + if (me_failed(me)) return ES_FAILED; + congruence_modified |= me_modified(me); + } + { + ModEvent me=x[free].lq(home,static_cast(u)); + if (me_failed(me)) return ES_FAILED; + congruence_modified |= me_modified(me); + } + } + } + + if (product_mod_assigned(x,m,residue)) { + GECODE_ME_CHECK(y.eq(home,residue)); + return home.ES_SUBSUMED(*this); + } + return congruence_modified ? ES_NOFIX : ES_FIX; } inline bool product_mod_var_enumerable(const ViewArray& x, const IntView& m, const IntView& y) { + const unsigned long long support_limit=200000ULL; unsigned long long n = static_cast(m.size()) * y.size(); - if (n > product_mod_support_limit) + if (n > support_limit) return false; for (int i=0; i product_mod_support_limit / s)) + if ((s != 0ULL) && (n > support_limit / s)) return false; n *= s; } - return n <= product_mod_support_limit; + return n <= support_limit; } inline bool @@ -548,6 +676,17 @@ namespace Gecode { namespace Int { namespace Arithmetic { return ES_OK; } } + switch (product_mod_status(x,y,m)) { + case RT_TRUE: + if (rm != RM_IMP) GECODE_ME_CHECK(b.one(home)); + return ES_OK; + case RT_FALSE: + if (rm != RM_PMI) GECODE_ME_CHECK(b.zero(home)); + return ES_OK; + case RT_MAYBE: + break; + default: GECODE_NEVER; + } (void) new (home) ReProductMod(home,x,m,y,b); return ES_OK; } @@ -590,31 +729,24 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (b.zero()) { if (rm == RM_IMP) return home.ES_SUBSUMED(*this); - if (product_mod_enumerable(x,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_mod_supports - (home,x,y,m,false,true,ht,hf)); - if (!ht) - return home.ES_SUBSUMED(*this); + switch (product_mod_status(x,y,m)) { + case RT_TRUE: return ES_FAILED; + case RT_FALSE: return home.ES_SUBSUMED(*this); + case RT_MAYBE: return ES_FIX; + default: GECODE_NEVER; } - return ES_FIX; } - if (product_mod_enumerable(x,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_mod_supports - (home,x,y,m,true,false,ht,hf)); - if (!ht) { - if (rm != RM_PMI) - GECODE_ME_CHECK(b.zero_none(home)); - return home.ES_SUBSUMED(*this); - } - if (!hf) { - if (rm != RM_IMP) - GECODE_ME_CHECK(b.one_none(home)); - return home.ES_SUBSUMED(*this); - } + switch (product_mod_status(x,y,m)) { + case RT_TRUE: + if (rm != RM_IMP) GECODE_ME_CHECK(b.one_none(home)); + return home.ES_SUBSUMED(*this); + case RT_FALSE: + if (rm != RM_PMI) GECODE_ME_CHECK(b.zero_none(home)); + return home.ES_SUBSUMED(*this); + case RT_MAYBE: + return ES_FIX; + default: GECODE_NEVER; } - return ES_FIX; } template diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index db95153c46..afe807ac80 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -353,7 +353,7 @@ namespace Test { namespace Int { ProductModXYZR(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::XYZR::"+str(ipl)+"::"+s, - 4,d,true,ipl), m(m0) {} + 4,d,true,ipl), m(m0) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return product_mod_value(x,3,m) == x[3]; } @@ -376,7 +376,7 @@ namespace Test { namespace Int { ProductModEmpty(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::Empty::"+str(ipl)+"::"+s, - 1,d,true,ipl), m(m0) {} + 1,d,true,ipl), m(m0) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == (1 % m); } @@ -397,7 +397,7 @@ namespace Test { namespace Int { ProductModSingleton(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::Singleton::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) {} + 2,d,true,ipl), m(m0) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { int r = x[0] % m; if (r < 0) @@ -421,7 +421,7 @@ namespace Test { namespace Int { ProductModXXYAlias(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::XXYAlias::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) {} + 2,d,true,ipl), m(m0) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { long long int a = x[0] % m; long long int b = x[1] % m; @@ -440,13 +440,108 @@ namespace Test { namespace Int { } }; + /// Targeted algebraic and quotient-band propagation for fixed modulus + class ProductModAlgebraic : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + ProductModAlgebraic(void) + : ::Test::Base("Int::Arithmetic::ProductMod::Algebraic") {} + virtual bool run(void) { + using namespace Gecode; + { + TestSpace home; + IntVarArgs x(home,3,-100,100); + IntVar y(home,-10,10); + product_mod(home,x,1,y); + if ((home.status() == SS_FAILED) || !y.assigned() || + (y.val() != 0)) + return false; + } + { + TestSpace home; + IntVar z(home,14,14), x(home,-1000,1000), y(home,0,6); + product_mod(home,IntVarArgs({z,x}),7,y); + if ((home.status() == SS_FAILED) || !y.assigned() || + (y.val() != 0)) + return false; + } + { + TestSpace home; + IntVar one(home,1,1), x(home,20,30), y(home,0,99); + product_mod(home,IntVarArgs({one,x}),100,y); + if ((home.status() == SS_FAILED) || + (y.min() != 20) || (y.max() != 30)) + return false; + } + { + TestSpace home; + IntVar x(home,15,19), y(home,0,6); + product_mod(home,IntVarArgs({x}),7,y); + if ((home.status() == SS_FAILED) || + (y.min() != 1) || (y.max() != 5)) + return false; + } + { + TestSpace home; + IntVar x(home,-20,-16), y(home,0,6); + product_mod(home,IntVarArgs({x}),7,y); + if ((home.status() == SS_FAILED) || + (y.min() != 1) || (y.max() != 5)) + return false; + } + { + TestSpace home; + IntVar c(home,6,6), x(home,-100,100), y(home,9,9); + product_mod(home,IntVarArgs({c,x}),15,y); + if ((home.status() == SS_FAILED) || + (x.min() != -96) || (x.max() != 99)) + return false; + } + { + TestSpace home; + IntVar c(home,6,6), x(home,-100,100), y(home,8,8); + product_mod(home,IntVarArgs({c,x}),15,y); + if (home.status() != SS_FAILED) + return false; + } + { + // The former Cartesian cutoff is exceeded by several orders of + // magnitude, but the zero algebra remains immediate. + TestSpace home; + IntVar zero(home,0,0); + IntVarArgs x(home,4,-100,100); + x << zero; + IntVar y(home,0,96); + product_mod(home,x,97,y); + if ((home.status() == SS_FAILED) || !y.assigned() || + (y.val() != 0)) + return false; + } + { + TestSpace home; + IntVar z(home,0,0), x(home,-100,100), y(home,0,1); + BoolVar b(home,0,1); + product_mod(home,IntVarArgs({z,x}),7,y,Reify(b)); + rel(home,y,IRT_EQ,0); + if ((home.status() == SS_FAILED) || !b.assigned() || + (b.val() != 1)) + return false; + } + return true; + } + }; + /// %Test for an ordinary two-factor product with a variable modulus class ProductModVarXYMR : public Test { public: ProductModVarXYMR(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::XYMR::"+str(ipl)+"::"+s, - 4,d,true,ipl) {} + 4,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return (x[2] > 0) && (product_mod_value(x,2,x[2]) == x[3]); @@ -468,7 +563,7 @@ namespace Test { namespace Int { ProductModVarEmpty(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Empty::"+str(ipl)+"::"+s, - 2,d,true,ipl) {} + 2,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == (1 % x[0])); } @@ -487,7 +582,7 @@ namespace Test { namespace Int { ProductModVarSingleton(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Singleton::"+str(ipl)+"::"+s, - 3,d,true,ipl) {} + 3,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -513,7 +608,7 @@ namespace Test { namespace Int { ProductModVarRepeated(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Repeated::"+str(ipl)+"::"+s, - 3,d,true,ipl) {} + 3,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -539,7 +634,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModFactorAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == 0); } @@ -561,7 +656,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::FactorResultAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -587,7 +682,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModResultAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) {} + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment&) const { return false; } virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), @@ -1941,6 +2036,7 @@ namespace Test { namespace Int { (void) new ArgMinBoolShared(i,false); } (void) new ProductModInvalidModulus; + (void) new ProductModAlgebraic; (void) new ProductModVarBounds; (void) new ProductModVarInactive; (void) new ProductBoundsLarge; From 78ab3314498c2ca5df49ece268a1e0decaef41e0 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 14:33:50 +0200 Subject: [PATCH 12/35] Strengthen variable product-mod propagation Zdev-Change-Id: Za127c189c6c2a9ffc8b003a76cf072708ba0e107d9cf5557d48ba73a712c32bc --- .zd/gcd/TASKS.md | 6 +- .zd/gcd/brief.md | 5 + ...ic-propagation-for-variable-product-mod.md | 26 +- gecode/int/arithmetic/product-mod.hpp | 302 ++++++++++-------- test/int/arithmetic.cpp | 120 +++++++ 5 files changed, 309 insertions(+), 150 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 30cabc5cca..afa251fd01 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,9 +3,9 @@ # Tasks: gcd - Total: 13 -- Ready: 3 +- Ready: 2 - Blocked: 2 -- Done: 8 +- Done: 9 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -20,5 +20,5 @@ | [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | | [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | | [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | -| [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | ready | — | +| [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | done | — | | [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | blocked | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index 7c141f9b32..d932fe8025 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -108,6 +108,11 @@ public APIs, and focused regression coverage. congruence, and algebraic rewrites. A fully assigned relation may be evaluated directly; reified relations otherwise remain conservative when entailment or disentailment cannot be established algebraically. +- Divisibility and congruence propagation should prefer interval tightening + and algebraic rewrites. Do not materialize a large sparse support set or + punch many holes into an integer domain merely because exact divisors or + residues can be generated; retain conservative bounds and perform exact + checking at assignment unless interior removal has a clear, bounded benefit. ## Open questions diff --git a/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md b/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md index 0c4967f0b7..14c11f3b47 100644 --- a/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md +++ b/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-012" key = "product-mod-variable-specialisation" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Add algebraic propagation for variable product_mod @@ -21,13 +21,13 @@ Variable-modulus product_mod derives useful modulus and result information from ## Done when -- [ ] A factor aliased with the modulus forces canonical result zero in the positive relation, while modulus=result remains correctly impossible. -- [ ] The empty product propagates the piecewise identity m=1 implies result=0 and m>=2 implies result=1, including backward pruning from the result. -- [ ] For assigned factors and result, modulus candidates are filtered by m>result and divisibility of product-result; product=result preserves every modulus above result. -- [ ] A zero result exposes sound modulus-divides-product reasoning where the product is safely known, without requiring Cartesian enumeration. -- [ ] Reified false and unfixed paths treat invalid modulus/result combinations correctly and leave inactive implications untouched. -- [ ] Focused tests cover empty products, assigned product/difference divisors, factor-modulus aliases, zero result, sparse and large modulus domains, all reification modes, cloning, and arithmetic limits. -- [ ] The affected integer library and focused ProductModVar tests build and pass. +- [x] A factor aliased with the modulus forces canonical result zero in the positive relation, while modulus=result remains correctly impossible. +- [x] The empty product propagates the piecewise identity m=1 implies result=0 and m>=2 implies result=1, including backward pruning from the result. +- [x] For assigned factors and result, divisor reasoning tightens modulus bounds and detects absence of a feasible divisor without scanning the modulus domain or creating a large set of holes; product=result preserves every modulus above result. +- [x] A zero result exposes sound modulus-divides-product bounds where the product is safely known, without Cartesian enumeration or projecting every divisor into the modulus domain. +- [x] Reified false and unfixed paths treat invalid modulus/result combinations correctly and leave inactive implications untouched. +- [x] Focused tests cover empty products, assigned product/difference divisors, factor-modulus aliases, zero result, sparse and large modulus domains, all reification modes, cloning, and arithmetic limits. +- [x] The affected integer library and focused ProductModVar tests build and pass. ## Validation @@ -35,3 +35,13 @@ Variable-modulus product_mod derives useful modulus and result information from - Run focused Int::Arithmetic::ProductModVar algebraic tests with multiple iterations. - Run fixed-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Replaced variable-modulus Cartesian support filtering with algebraic empty/alias/status reasoning and bounds-only divisor extrema, avoiding dense-domain scans and mass hole creation. + +Validation: + +- Built the integer library and gecode-test; ProductModVar passed three focused iterations and fixed ProductMod regressions passed. +- Broader Int::Arithmetic passed; dense full-range modulus smoke completed in 0.01 seconds with sound bound propagation. +- Independent spec and standards verification passed; git diff --check and zd check passed. diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 8874bf140f..0fbe4d7405 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -323,136 +323,119 @@ namespace Gecode { namespace Int { namespace Arithmetic { return congruence_modified ? ES_NOFIX : ES_FIX; } + /// Whether the modulus occurs among the factors. inline bool - product_mod_var_enumerable(const ViewArray& x, - const IntView& m, const IntView& y) { - const unsigned long long support_limit=200000ULL; - unsigned long long n = static_cast(m.size()) * y.size(); - if (n > support_limit) - return false; - for (int i=0; i support_limit / s)) - return false; - n *= s; - } - return n <= support_limit; + product_mod_var_mod_factor(const ViewArray& x, const IntView& m) { + for (int i=0; i& x, - const IntView& m, const IntView& y, - const int* v) { - const int mi = x.size(); - const int yi = mi+1; - if ((m == y) && (v[mi] != v[yi])) - return false; + product_mod_var_exact(const ViewArray& x, long long int& p) { + long long int q=1; for (int i=0; i& x, - IntView m, IntView y, bool positive=true, - bool filter=true, bool* has_true_out=nullptr, - bool* has_false_out=nullptr) { - const int n = x.size(); - const int nv = n+2; - Region r; - unsigned int* size = r.alloc(nv); - unsigned int* pos = r.alloc(nv); - int** values = r.alloc(nv); - unsigned long long cap64 = static_cast(m.size()) * - y.size(); - for (int i=0; i(cap64); - int** support = filter ? r.alloc(nv) : nullptr; - - for (int i=0; i(size[i]); - unsigned int k=0; - for (ViewValues vi(x[i]); vi(); ++vi) - values[i][k++] = vi.val(); - if (filter) support[i] = r.alloc(cap); - } - size[n] = m.size(); pos[n] = 0; - values[n] = r.alloc(size[n]); - unsigned int k=0; - for (ViewValues vi(m); vi(); ++vi) - values[n][k++] = vi.val(); - if (filter) support[n] = r.alloc(cap); - size[n+1] = y.size(); pos[n+1] = 0; - values[n+1] = r.alloc(size[n+1]); - k=0; - for (ViewValues vi(y); vi(); ++vi) - values[n+1][k++] = vi.val(); - if (filter) support[n+1] = r.alloc(cap); - - int* tuple = r.alloc(nv); - unsigned int ns = 0; - bool has_true = false, has_false = false; - bool more = true; - while (more) { - for (int i=0; i 0) && (tuple[n+1] >= 0) && - (tuple[n+1] < tuple[n]) && - (product_mod_value(tuple,n,tuple[n]) == tuple[n+1]); - has_true |= relation; has_false |= !relation; - if (filter && (relation == positive)) { - for (int i=0; i(-(d+1))+1ULL + : static_cast(d); + bool found=false; + for (unsigned long long int q=1; q <= ad/q; q++) + if ((ad % q) == 0) { + const unsigned long long int r=ad/q; + if ((q <= static_cast(Limits::max)) && + (q >= static_cast(lower)) && + (q <= static_cast(upper))) { + const int v=static_cast(q); + if (!found) least=greatest=v; + else { least=std::min(least,v); greatest=std::max(greatest,v); } + found=true; + } + if ((r != q) && + (r <= static_cast(Limits::max)) && + (r >= static_cast(lower)) && + (r <= static_cast(upper))) { + const int v=static_cast(r); + if (!found) least=greatest=v; + else { least=std::min(least,v); greatest=std::max(greatest,v); } + found=true; } } - int i=nv-1; - while ((i >= 0) && (++pos[i] == size[i])) { - pos[i]=0; i--; - } - more = i >= 0; - } + return found; + } - if (has_true_out != nullptr) *has_true_out = has_true; - if (has_false_out != nullptr) *has_false_out = has_false; - if (!filter) - return ES_OK; - if (ns == 0) - return ES_FAILED; - for (int i=0; i& x, const IntView& m, + const IntView& y) { + if ((m == y) || (m.max() <= 0) || (y.max() < 0) || + (y.min() >= m.max())) + return RT_FALSE; + const bool mf=product_mod_var_mod_factor(x,m); + if (mf) { + if (!y.in(0)) return RT_FALSE; + if ((m.min() > 0) && y.assigned()) return RT_TRUE; + return RT_MAYBE; + } + if (x.size() == 0) { + const bool zero=m.in(1) && y.in(0); + const bool one=(m.max() >= 2) && y.in(1); + if (!zero && !one) return RT_FALSE; + if ((m.min() > 0) && + ((m.assigned() && (m.val() == 1) && y.assigned() && + (y.val() == 0)) || + ((m.min() >= 2) && y.assigned() && (y.val() == 1)))) + return RT_TRUE; + return RT_MAYBE; + } + if (y.assigned()) { + long long int p; + if (product_mod_var_exact(x,p) && + !Limits::overflow_sub(p,static_cast(y.val()))) { + const long long int d=p-y.val(); + if (d == 0) + return (y.val() >= 0) && (m.min() > y.val()) + ? RT_TRUE : RT_MAYBE; + int least, greatest; + if (!product_mod_var_divisor_bounds + (std::max(m.min(),y.val()+1),m.max(),d,least,greatest)) + return RT_FALSE; } } - return ES_OK; + bool assigned=m.assigned() && y.assigned(); + for (int i=0; assigned && (i= m.val())) + return RT_FALSE; + int residue; + (void) product_mod_assigned(x,m.val(),residue); + return residue == y.val() ? RT_TRUE : RT_FALSE; } forceinline ProductModVar::ProductModVar(Home home, ViewArray& z, IntView modulus, IntView w) : Propagator(home), x(z), m(modulus), y(w) { - x.subscribe(home,*this,PC_INT_DOM); - m.subscribe(home,*this,PC_INT_DOM); + x.subscribe(home,*this,PC_INT_VAL); + m.subscribe(home,*this,PC_INT_BND); y.subscribe(home,*this,PC_INT_DOM); } @@ -464,6 +447,20 @@ namespace Gecode { namespace Int { namespace Arithmetic { GECODE_ME_CHECK(y.gq(home,0)); GECODE_ME_CHECK(y.lq(home,m.max()-1)); GECODE_ME_CHECK(m.gq(home,y.min()+1)); + if (product_mod_var_mod_factor(x,m)) { + GECODE_ME_CHECK(y.eq(home,0)); + return ES_OK; + } + if (x.size() == 0) { + GECODE_ME_CHECK(y.lq(home,1)); + if (!y.in(0)) { + GECODE_ME_CHECK(m.gq(home,2)); + GECODE_ME_CHECK(y.eq(home,1)); + } else if (!y.in(1)) { + GECODE_ME_CHECK(m.eq(home,1)); + GECODE_ME_CHECK(y.eq(home,0)); + } + } if (m.assigned()) return ProductMod::post(home,x,m.val(),y); (void) new (home) ProductModVar(home,x,m,y); @@ -488,8 +485,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline void ProductModVar::reschedule(Space& home) { - x.reschedule(home,*this,PC_INT_DOM); - m.reschedule(home,*this,PC_INT_DOM); + x.reschedule(home,*this,PC_INT_VAL); + m.reschedule(home,*this,PC_INT_BND); y.reschedule(home,*this,PC_INT_DOM); } @@ -499,10 +496,54 @@ namespace Gecode { namespace Int { namespace Arithmetic { GECODE_ME_CHECK(y.gq(home,0)); GECODE_ME_CHECK(y.lq(home,m.max()-1)); GECODE_ME_CHECK(m.gq(home,y.min()+1)); + if (product_mod_var_mod_factor(x,m)) { + GECODE_ME_CHECK(y.eq(home,0)); + return home.ES_SUBSUMED(*this); + } + for (int i=0; i= 2) { + GECODE_ME_CHECK(y.eq(home,1)); + return home.ES_SUBSUMED(*this); + } + if (!m.in(1)) { + GECODE_ME_CHECK(y.eq(home,1)); + return home.ES_SUBSUMED(*this); + } + } if (m.assigned()) GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); - if (product_mod_var_enumerable(x,m,y)) - GECODE_ES_CHECK(product_mod_var_supports(home,x,m,y)); + + // With an assigned product and result, m must divide product-result. + long long int p; + if (y.assigned() && product_mod_var_exact(x,p)) { + if (!Limits::overflow_sub(p,static_cast(y.val()))) { + const long long int d=p-y.val(); + if (d == 0) + return home.ES_SUBSUMED(*this); + int least, greatest; + if (!product_mod_var_divisor_bounds + (m.min(),m.max(),d,least,greatest)) + return ES_FAILED; + GECODE_ME_CHECK(m.gq(home,least)); + GECODE_ME_CHECK(m.lq(home,greatest)); + } + } if (m.assigned()) GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); bool assigned = m.assigned() && y.assigned(); @@ -515,8 +556,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline size_t ProductModVar::dispose(Space& home) { - x.cancel(home,*this,PC_INT_DOM); - m.cancel(home,*this,PC_INT_DOM); + x.cancel(home,*this,PC_INT_VAL); + m.cancel(home,*this,PC_INT_BND); y.cancel(home,*this,PC_INT_DOM); (void) Propagator::dispose(home); return sizeof(*this); @@ -527,7 +568,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { ReProductModVar::ReProductModVar(Home home, ViewArray& z, IntView modulus, IntView w, BoolView c) : Propagator(home), x(z), m(modulus), y(w), b(c) { - x.subscribe(home,*this,PC_INT_DOM); + x.subscribe(home,*this,PC_INT_VAL); m.subscribe(home,*this,PC_INT_DOM); y.subscribe(home,*this,PC_INT_DOM); b.subscribe(home,*this,PC_BOOL_VAL); @@ -571,7 +612,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline void ReProductModVar::reschedule(Space& home) { - x.reschedule(home,*this,PC_INT_DOM); + x.reschedule(home,*this,PC_INT_VAL); m.reschedule(home,*this,PC_INT_DOM); y.reschedule(home,*this,PC_INT_DOM); b.reschedule(home,*this,PC_BOOL_VAL); @@ -588,32 +629,15 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (b.zero()) { if (rm == RM_IMP) return home.ES_SUBSUMED(*this); - if (product_mod_var_enumerable(x,m,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_mod_var_supports - (home,x,m,y,false,true,&ht,&hf)); - if (!ht) - return home.ES_SUBSUMED(*this); - } else if ((m == y) || (m.max() <= 0) || (y.max() < 0) || - (y.min() >= m.max())) { + const RelTest rt=product_mod_var_status(x,m,y); + if (rt == RT_TRUE) + return ES_FAILED; + if (rt == RT_FALSE) return home.ES_SUBSUMED(*this); - } return ES_FIX; } - RelTest rt; - if (m == y) { - rt = RT_FALSE; - } else if ((m.max() <= 0) || (y.max() < 0) || (y.min() >= m.max())) { - rt = RT_FALSE; - } else if (product_mod_var_enumerable(x,m,y)) { - bool ht, hf; - GECODE_ES_CHECK(product_mod_var_supports - (home,x,m,y,true,false,&ht,&hf)); - rt = !ht ? RT_FALSE : (!hf ? RT_TRUE : RT_MAYBE); - } else { - rt = RT_MAYBE; - } + const RelTest rt=product_mod_var_status(x,m,y); switch (rt) { case RT_TRUE: if (rm != RM_IMP) GECODE_ME_CHECK(b.one_none(home)); @@ -630,7 +654,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline size_t ReProductModVar::dispose(Space& home) { - x.cancel(home,*this,PC_INT_DOM); + x.cancel(home,*this,PC_INT_VAL); m.cancel(home,*this,PC_INT_DOM); y.cancel(home,*this,PC_INT_DOM); b.cancel(home,*this,PC_BOOL_VAL); diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index afe807ac80..8f0f529301 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -730,6 +730,125 @@ namespace Test { namespace Int { } }; + /// Targeted algebraic propagation for a variable modulus + class ProductModVarAlgebraic : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + static bool domain(const Gecode::IntVar& x, const int* v, int n) { + if (x.size() != static_cast(n)) + return false; + for (int i=0; i Date: Wed, 12 Aug 2026 14:44:49 +0200 Subject: [PATCH 13/35] Clarify enumeration-free GCD and divides Zdev-Change-Id: Zeae09906bc1aa34439646b3b0d81635225d236cbba0e645a84d570635f022a3d --- .zd/gcd/brief.md | 7 +++++++ ...e-support-enumeration-from-number-theoretic-propagat.md | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index d932fe8025..22caee7c06 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -108,6 +108,13 @@ public APIs, and focused regression coverage. congruence, and algebraic rewrites. A fully assigned relation may be evaluated directly; reified relations otherwise remain conservative when entailment or disentailment cannot be established algebraically. +- In particular, ordinary and reified GCD and reified divides must not use + small-domain enumeration as a stronger propagation level. Their propagation + and reified status use bounds, zero/sign cases, divisibility, gcd identities, + and conservative entailment or disentailment when algebra cannot decide. +- Computing a single mathematically determined output from assigned inputs is + algebraic propagation and is permitted; enumerating combinations of + unassigned domains or projecting their supports is not. - Divisibility and congruence propagation should prefer interval tightening and algebraic rewrites. Do not materialize a large sparse support set or punch many holes into an integer domain merely because exact divisors or diff --git a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md index 1627880a76..d9c697e2eb 100644 --- a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md +++ b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md @@ -15,13 +15,15 @@ Every propagator added in the gcd area uses bounds or algebraic reasoning instea ## Boundaries - Audit Gcd/ReGcd, ReDivides, Product/ReProduct, ProductMod/ReProductMod, and ProductModVar/ReProductModVar, including shared helpers. +- Gcd/ReGcd and ReDivides must remove their 200,000-tuple thresholds, Cartesian domain scans, support arrays, and projected filtering rather than retaining enumeration for small domains. - Do not change the public relation semantics or add proof-logging artifacts. -- Direct evaluation is allowed only when all variables relevant to the relation are assigned; do not collect or project tuple supports. +- Direct evaluation of a complete relation is allowed when all relevant variables are assigned. Computing one determined output from assigned inputs is also allowed; do not enumerate combinations of unassigned domains or collect/project tuple supports. - Use the existing integer arithmetic test harness and the focused testing level in the area brief. ## Done when - [ ] No audited propagator or helper enumerates a Cartesian product of variable domains, allocates tuple-support tables, or filters domains by projected tuple supports. +- [ ] Gcd/ReGcd and ReDivides use bounds and number-theoretic identities for propagation and conservative algebraic reification status, with no small-domain enumeration fallback. - [ ] Ordinary propagation is expressed through sound bounds, sign/zero classification, divisibility, congruence, or algebraic rewrites, with conservative behavior where those deductions are inconclusive. - [ ] Reified entailment and disentailment use sound bounds or algebraic tests and exact evaluation only for fully assigned relations; negative or unfixed cases wait conservatively instead of enumerating supports. - [ ] Propagation conditions, subscriptions, and costs match the information actually used, including bounds subscriptions where interior-domain events are no longer required. From b5e36ce8888d937ae241573a0d930e105bc0ba87 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 15:11:09 +0200 Subject: [PATCH 14/35] Complete zero-aware product bounds Zdev-Change-Id: Z0a96802fefc135e7804e2354bf001ee704b921ca3c73374a22028e3e6de0ed9b --- .zd/gcd/TASKS.md | 6 +- ...-aware-inverse-bounds-for-n-ary-product.md | 23 +++- gecode/int/arithmetic.hh | 6 +- gecode/int/arithmetic/product.hpp | 127 ++++++++++++------ test/int/arithmetic.cpp | 63 +++++++++ 5 files changed, 173 insertions(+), 52 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index afa251fd01..1207b5c997 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,9 +3,9 @@ # Tasks: gcd - Total: 13 -- Ready: 2 +- Ready: 1 - Blocked: 2 -- Done: 9 +- Done: 10 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -16,7 +16,7 @@ | [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | | [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | | [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | -| [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | ready | — | +| [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | done | — | | [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | | [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | | [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | diff --git a/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md b/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md index cdfdfdbb1f..2e24d33088 100644 --- a/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md +++ b/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-008" key = "product-zero-aware-inverse-bounds" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Complete zero-aware inverse bounds for n-ary product @@ -21,12 +21,12 @@ The exact n-ary Product actor performs sound backward bounds propagation for pos ## Done when -- [ ] Backward factor propagation implements the full zero-aware quotient case split: no filter when cofactor and result both contain zero, failure when a zero cofactor cannot produce the result, magnitude bounds when a cofactor spans zero and the result excludes zero, and directed floor/ceiling quotient bounds for one-sided cofactors. -- [ ] Positive, nonnegative, negative, nonpositive, mixed, and fixed-zero domain combinations produce sound bounds without division by zero or signed overflow. -- [ ] Cofactor interval computation is structured to avoid unnecessary repeated whole-array recomputation while preserving safe saturation and alias behavior. -- [ ] Ordinary propagation and positive reified rewrites reach an honest fixpoint with correct subscriptions, costs, cloning, and subsumption. -- [ ] Focused tests exercise every sign/zero case, large domains, aliases, and overflow boundaries through the existing Product harness. -- [ ] The affected integer library and focused Product tests build and pass. +- [x] Backward factor propagation implements the full zero-aware quotient case split: no filter when cofactor and result both contain zero, failure when a zero cofactor cannot produce the result, magnitude bounds when a cofactor spans zero and the result excludes zero, and directed floor/ceiling quotient bounds for one-sided cofactors. +- [x] Positive, nonnegative, negative, nonpositive, mixed, and fixed-zero domain combinations produce sound bounds without division by zero or signed overflow. +- [x] Cofactor interval computation is structured to avoid unnecessary repeated whole-array recomputation while preserving safe saturation and alias behavior. +- [x] Ordinary propagation and positive reified rewrites reach an honest fixpoint with correct subscriptions, costs, cloning, and subsumption. +- [x] Focused tests exercise every sign/zero case, large domains, aliases, and overflow boundaries through the existing Product harness. +- [x] The affected integer library and focused Product tests build and pass. ## Validation @@ -34,3 +34,12 @@ The exact n-ary Product actor performs sound backward bounds propagation for pos - Run focused Int::Arithmetic::Product sign-class and inverse-bound tests with multiple iterations. - Run the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Added complete zero-aware inverse bounds for n-ary Product using prefix/suffix cofactor intervals and safe quotient hulls for all sign classes. + +Validation: + +- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. +- Independent spec and standards verification, exhaustive small-interval diagnostics, git diff --check, and zd check passed. diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 5a0f77b82e..44bb6a8289 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -864,10 +864,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { /** \brief Bounds propagator for an exact n-ary product * \ingroup FuncIntProp */ - class Product : public NaryOnePropagator { + class Product : public NaryOnePropagator { protected: - using NaryOnePropagator::x; - using NaryOnePropagator::y; + using NaryOnePropagator::x; + using NaryOnePropagator::y; Product(Home home, ViewArray& x, IntView y); Product(Space& home, Product& p); public: diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index ef72d60abb..ddd103c8ba 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -59,6 +59,46 @@ namespace Gecode { namespace Int { namespace Arithmetic { return r; } + /// Multiply two representable intervals. + forceinline ProductInterval + product_interval_mul(ProductInterval a, ProductInterval b) { + return product_interval_mul(a,b.min,b.max); + } + + /// Compute the hull of quotients by a zero-free interval. + forceinline ProductInterval + product_quotient_interval(int ymin, int ymax, + int qmin, int qmax) { + long long int c[4] = { + ceil_div_xx(static_cast(ymin), + static_cast(qmin)), + ceil_div_xx(static_cast(ymin), + static_cast(qmax)), + ceil_div_xx(static_cast(ymax), + static_cast(qmin)), + ceil_div_xx(static_cast(ymax), + static_cast(qmax)) + }; + long long int f[4] = { + floor_div_xx(static_cast(ymin), + static_cast(qmin)), + floor_div_xx(static_cast(ymin), + static_cast(qmax)), + floor_div_xx(static_cast(ymax), + static_cast(qmin)), + floor_div_xx(static_cast(ymax), + static_cast(qmax)) + }; + const long long int l = std::max + (static_cast(Limits::min),std::min + (*std::min_element(c,c+4),static_cast(Limits::max))); + const long long int u = std::max + (static_cast(Limits::min),std::min + (*std::max_element(f,f+4),static_cast(Limits::max))); + ProductInterval r = {static_cast(l),static_cast(u)}; + return r; + } + /// Compute product bounds, optionally omitting one factor. inline ProductInterval product_interval(const ViewArray& x, int omit=-1) { @@ -111,7 +151,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline Product::Product(Home home, ViewArray& z, IntView w) - : NaryOnePropagator(home,z,w) {} + : NaryOnePropagator(home,z,w) {} inline ExecStatus Product::post(Home home, ViewArray& x, IntView y) { @@ -125,7 +165,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline Product::Product(Space& home, Product& p) - : NaryOnePropagator(home,p) {} + : NaryOnePropagator(home,p) {} forceinline Actor* Product::copy(Space& home) { @@ -155,46 +195,55 @@ namespace Gecode { namespace Int { namespace Arithmetic { modified |= me_modified(me); } + // Prefix and suffix products provide every omitted-factor interval in + // linear time. They are rebuilt after each narrowing pass. + Region r; + ProductInterval* prefix = r.alloc(x.size()+1); + ProductInterval* suffix = r.alloc(x.size()+1); + prefix[0] = ProductInterval{1,1}; + for (int i=0; i 0) || (q.max < 0)) { - long long int c[4] = { - ceil_div_xx(static_cast(y.min()), - static_cast(q.min)), - ceil_div_xx(static_cast(y.min()), - static_cast(q.max)), - ceil_div_xx(static_cast(y.max()), - static_cast(q.min)), - ceil_div_xx(static_cast(y.max()), - static_cast(q.max)) - }; - long long int f[4] = { - floor_div_xx(static_cast(y.min()), - static_cast(q.min)), - floor_div_xx(static_cast(y.min()), - static_cast(q.max)), - floor_div_xx(static_cast(y.max()), - static_cast(q.min)), - floor_div_xx(static_cast(y.max()), - static_cast(q.max)) - }; - long long int l = *std::min_element(c,c+4); - long long int u = *std::max_element(f,f+4); - l = std::max(l,static_cast(Limits::min)); - u = std::min(u,static_cast(Limits::max)); - if (l > u) - return ES_FAILED; - { - ModEvent me = x[i].gq(home,static_cast(l)); - if (me_failed(me)) return ES_FAILED; - modified |= me_modified(me); - } - { - ModEvent me = x[i].lq(home,static_cast(u)); - if (me_failed(me)) return ES_FAILED; - modified |= me_modified(me); + ProductInterval q = product_interval_mul(prefix[i],suffix[i+1]); + if ((q.min <= 0) && (q.max >= 0) && y.in(0)) + continue; + if ((q.min == 0) && (q.max == 0)) + return ES_FAILED; + + bool have=false; + ProductInterval d = {Limits::max,Limits::min}; + if (q.min < 0) { + ProductInterval n = product_quotient_interval + (y.min(),y.max(),q.min,std::min(q.max,-1)); + d=n; have=true; + } + if (q.max > 0) { + ProductInterval p = product_quotient_interval + (y.min(),y.max(),std::max(q.min,1),q.max); + if (have) { + d.min=std::min(d.min,p.min); d.max=std::max(d.max,p.max); + } else { + d=p; have=true; } } + if (!have || (d.min > d.max)) + return ES_FAILED; + { + ModEvent me = x[i].gq(home,d.min); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me = x[i].lq(home,d.max); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } } } while (modified); diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 8f0f529301..6cf2c29ceb 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -333,6 +333,68 @@ namespace Test { namespace Int { } }; + /// Test zero-aware inverse bounds for every cofactor sign class + class ProductInverseBounds : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + static bool bounds(const Gecode::IntVar& x, int min, int max) { + return (x.min() == min) && (x.max() == max); + } + public: + ProductInverseBounds(void) + : ::Test::Base("Int::Arithmetic::Product::InverseBounds") {} + virtual bool run(void) { + using namespace Gecode; + struct Case { int qmin; int qmax; int xmin; int xmax; }; + const Case cases[] = { + { 2, 4, 5, 12}, // Positive + { 0, 4, 5, 24}, // Nonnegative + {-4,-2, -12, -5}, // Negative + {-4, 0, -24, -5}, // Nonpositive + {-4, 3, -24, 24} // Mixed across zero + }; + for (unsigned int i=0; i Date: Wed, 12 Aug 2026 15:31:17 +0200 Subject: [PATCH 15/35] Simplify product signs and units Zdev-Change-Id: Z6697471e381aaf89269dffc32cf564d9ce1b5f3b6083b2f7c128f70450637b3a --- .zd/gcd/TASKS.md | 8 +- .zd/gcd/brief.md | 5 + ...nit-and-sign-rewrites-for-n-ary-product.md | 23 +++- gecode/int/arithmetic.hh | 13 +- gecode/int/arithmetic/product.hpp | 128 ++++++++++++++++-- test/int/arithmetic.cpp | 116 +++++++++++++++- 6 files changed, 262 insertions(+), 31 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 1207b5c997..e099887091 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -4,8 +4,8 @@ - Total: 13 - Ready: 1 -- Blocked: 2 -- Done: 10 +- Blocked: 1 +- Done: 11 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -17,8 +17,8 @@ | [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | | [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | | [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | done | — | -| [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | ready | — | -| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | blocked | gcd-009 | +| [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | done | — | +| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | ready | gcd-009 | | [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | | [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | done | — | | [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | blocked | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md index 22caee7c06..c6f51e31a5 100644 --- a/.zd/gcd/brief.md +++ b/.zd/gcd/brief.md @@ -120,6 +120,11 @@ public APIs, and focused regression coverage. punch many holes into an integer domain merely because exact divisors or residues can be generated; retain conservative bounds and perform exact checking at assignment unless interior removal has a clear, bounded benefit. +- Prefer bounds subscriptions for bounds propagators even when an interior + domain change, such as removal of zero, could enable stronger pruning. + Missing that wakeup is acceptable weak monotonicity; do not subscribe to all + domain changes solely to observe it. Use the repository's weakly monotonic + execution-status convention so propagation remains lifecycle-correct. ## Open questions diff --git a/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md b/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md index 30271de9a3..eb9f29850b 100644 --- a/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md +++ b/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-009" key = "product-zero-unit-sign-rewrites" area = "gcd" -status = "open" +status = "done" blocked_by = [] +++ # Add zero, unit, and sign rewrites for n-ary product @@ -21,12 +21,12 @@ Exact product posting and propagation simplify assigned zero, one, and minus-one ## Done when -- [ ] A fixed-zero factor assigns the positive relation's result to zero; assigned one factors are removed; assigned minus-one factors are removed with the result sign transformed safely. -- [ ] If the result excludes zero, bounds-visible zero endpoints are removed from nonnegative or nonpositive factors, and a zero result with exactly one possible zero source propagates that source to zero when sound. -- [ ] Aggregate sign parity and zero possibility constrain the result sign and permit safe normalisation or rewriting to a positive-magnitude path when all remaining factors have strict signs. -- [ ] Reified actors apply these deductions only when truth is required and preserve no-narrowing inactive implication behavior. -- [ ] Focused tests cover zero, one, minus one, strict and non-strict sign combinations, sign parity, reification controls, cloning, and representable limits. -- [ ] The affected integer library and focused Product tests build and pass. +- [x] A fixed-zero factor assigns the positive relation's result to zero; assigned one factors are removed; assigned minus-one factors are removed with the result sign transformed safely. +- [x] If the result excludes zero, bounds-visible zero endpoints are removed from nonnegative or nonpositive factors, and a zero result with exactly one possible zero source propagates that source to zero when sound. +- [x] Aggregate sign parity and zero possibility constrain the result sign and permit safe normalisation or rewriting to a positive-magnitude path when all remaining factors have strict signs. +- [x] Reified actors apply these deductions only when truth is required and preserve no-narrowing inactive implication behavior. +- [x] Focused tests cover zero, one, minus one, strict and non-strict sign combinations, sign parity, reification controls, cloning, and representable limits. +- [x] The affected integer library and focused Product tests build and pass. ## Validation @@ -34,3 +34,12 @@ Exact product posting and propagation simplify assigned zero, one, and minus-one - Run focused Int::Arithmetic::Product simplification and sign tests with multiple iterations. - Run the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Added zero/unit simplification, minus-unit parity, zero-source and aggregate-sign reasoning to Product using the bounds-subscribed weak-monotonic actor convention. + +Validation: + +- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. +- Independent spec/standards and explicit clone-disable-enable verification passed; git diff --check and zd check passed. diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 44bb6a8289..2c8a1c7300 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -864,17 +864,20 @@ namespace Gecode { namespace Int { namespace Arithmetic { /** \brief Bounds propagator for an exact n-ary product * \ingroup FuncIntProp */ - class Product : public NaryOnePropagator { + class Product : public NaryOnePropagator { protected: - using NaryOnePropagator::x; - using NaryOnePropagator::y; - Product(Home home, ViewArray& x, IntView y); + using NaryOnePropagator::x; + using NaryOnePropagator::y; + bool neg; + Product(Home home, ViewArray& x, IntView y, bool neg); Product(Space& home, Product& p); public: - static ExecStatus post(Home home, ViewArray& x, IntView y); + static ExecStatus post(Home home, ViewArray& x, IntView y, + bool neg=false); virtual Actor* copy(Space& home); virtual PropCost cost(const Space& home, const ModEventDelta& med) const; virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + virtual size_t dispose(Space& home); }; /** \brief Reified domain propagator for an exact n-ary product diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index ddd103c8ba..662960dc79 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -150,22 +150,36 @@ namespace Gecode { namespace Int { namespace Arithmetic { } forceinline - Product::Product(Home home, ViewArray& z, IntView w) - : NaryOnePropagator(home,z,w) {} + Product::Product(Home home, ViewArray& z, IntView w, bool n) + : NaryOnePropagator(home,z,w), neg(n) { + home.notice(*this,AP_WEAKLY); + } inline ExecStatus - Product::post(Home home, ViewArray& x, IntView y) { + Product::post(Home home, ViewArray& x, IntView y, bool neg) { + for (int i=x.size(); i--;) { + if (!x[i].assigned()) + continue; + if (x[i].val() == 0) { + GECODE_ME_CHECK(y.eq(home,0)); + return ES_OK; + } + if ((x[i].val() == 1) || (x[i].val() == -1)) { + neg ^= x[i].val() == -1; + x.move_lst(i); + } + } if (x.size() == 0) { - GECODE_ME_CHECK(y.eq(home,1)); + GECODE_ME_CHECK(y.eq(home,neg ? -1 : 1)); return ES_OK; } - (void) new (home) Product(home,x,y); + (void) new (home) Product(home,x,y,neg); return ES_OK; } forceinline Product::Product(Space& home, Product& p) - : NaryOnePropagator(home,p) {} + : NaryOnePropagator(home,p), neg(p.neg) {} forceinline Actor* Product::copy(Space& home) { @@ -177,20 +191,95 @@ namespace Gecode { namespace Int { namespace Arithmetic { return PropCost::quadratic(PropCost::LO,x.size()+1); } + forceinline size_t + Product::dispose(Space& home) { + home.ignore(*this,AP_WEAKLY); + (void) NaryOnePropagator::dispose(home); + return sizeof(*this); + } + inline ExecStatus Product::propagate(Space& home, const ModEventDelta&) { + // Absorb a fixed zero and rewrite when new units have appeared. + int units=0; + bool next_neg=neg; + for (int i=x.size(); i--;) { + if (!x[i].assigned()) + continue; + if (x[i].val() == 0) { + GECODE_ME_CHECK(y.eq(home,0)); + return home.ES_SUBSUMED(*this); + } + if ((x[i].val() == 1) || (x[i].val() == -1)) { + next_neg ^= x[i].val() == -1; + units++; + } + } + if (units > 0) { + ViewArray z(home,x.size()-units); + int j=0; + for (int i=0; i 0)) + GECODE_ME_CHECK(x[i].gq(home,1)); + else if ((x[i].max() == 0) && (x[i].min() < 0)) + GECODE_ME_CHECK(x[i].lq(home,-1)); + } + + // Zero can only arise from a zero factor. + if (y.assigned() && (y.val() == 0)) { + int zero=-1; + for (int i=0; i= 0) { zero=-2; break; } + zero=i; + } + if (zero == -1) + return ES_FAILED; + if (zero >= 0) { + GECODE_ME_CHECK(x[zero].eq(home,0)); + return home.ES_SUBSUMED(*this); + } + } + + // Stable one-sided signs determine aggregate parity and zero possibility. + bool stable=true, strict=true, negative=neg; + for (int i=0; i= 0) { + strict &= x[i].min() > 0; + } else { + stable=false; strict=false; break; + } + } + if (stable) { + if (negative) + GECODE_ME_CHECK(y.lq(home,strict ? -1 : 0)); + else + GECODE_ME_CHECK(y.gq(home,strict ? 1 : 0)); + } + bool modified; do { modified = false; ProductInterval p = product_interval(x); { - ModEvent me = y.gq(home,p.min); + ModEvent me = neg ? y.lq(home,-p.min) : y.gq(home,p.min); if (me_failed(me)) return ES_FAILED; modified |= me_modified(me); } { - ModEvent me = y.lq(home,p.max); + ModEvent me = neg ? y.gq(home,-p.max) : y.lq(home,p.max); if (me_failed(me)) return ES_FAILED; modified |= me_modified(me); } @@ -220,12 +309,14 @@ namespace Gecode { namespace Int { namespace Arithmetic { ProductInterval d = {Limits::max,Limits::min}; if (q.min < 0) { ProductInterval n = product_quotient_interval - (y.min(),y.max(),q.min,std::min(q.max,-1)); + (neg ? -y.max() : y.min(),neg ? -y.min() : y.max(), + q.min,std::min(q.max,-1)); d=n; have=true; } if (q.max > 0) { ProductInterval p = product_quotient_interval - (y.min(),y.max(),std::max(q.min,1),q.max); + (neg ? -y.max() : y.min(),neg ? -y.min() : y.max(), + std::max(q.min,1),q.max); if (have) { d.min=std::min(d.min,p.min); d.max=std::max(d.max,p.max); } else { @@ -247,6 +338,21 @@ namespace Gecode { namespace Int { namespace Arithmetic { } } while (modified); + // Result propagation above can have excluded zero during this call. + bool endpoint_modified=false; + if (!y.in(0)) + for (int i=0; i 0)) + me=x[i].gq(home,1); + else if ((x[i].max() == 0) && (x[i].min() < 0)) + me=x[i].lq(home,-1); + if (me_failed(me)) return ES_FAILED; + endpoint_modified |= me_modified(me); + } + if (endpoint_modified) + return ES_NOFIX; + bool factors_assigned = true; for (int i=0; factors_assigned && (i(x[0]) * x[0] * x[1]; return (p >= Gecode::Int::Limits::min) && @@ -333,6 +333,113 @@ namespace Test { namespace Int { } }; + /// Test zero, unit, and aggregate-sign product simplifications + class ProductSimplifySign : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + class CloneSpace : public Gecode::Space { + public: + Gecode::IntVar x, q, y; + CloneSpace(void) + : x(*this,-2,2), q(*this,2,2), y(*this,-10,10) { + Gecode::product(*this,Gecode::IntVarArgs({x,q}),y); + } + CloneSpace(CloneSpace& s) : Gecode::Space(s) { + x.update(*this,s.x); q.update(*this,s.q); y.update(*this,s.y); + } + virtual Gecode::Space* copy(void) { return new CloneSpace(*this); } + }; + public: + ProductSimplifySign(void) + : ::Test::Base("Int::Arithmetic::Product::SimplifySign") {} + virtual bool run(void) { + using namespace Gecode; + { + TestSpace home; + IntVar z(home,0,0), x(home,-100,100), y(home,-100,100); + product(home,IntVarArgs({x,z}),y); + if ((home.status() == SS_FAILED) || !y.assigned() || (y.val()!=0)) + return false; + } + { + TestSpace home; + IntVar one(home,1,1), minus(home,-1,-1), x(home,2,5); + IntVar y(home,-100,100); + product(home,IntVarArgs({one,minus,minus,x}),y); + if ((home.status() == SS_FAILED) || (y.min()!=2) || (y.max()!=5)) + return false; + } + { + TestSpace home; + IntVar minus(home,-1,-1), x(home,2,5), y(home,-100,100); + product(home,IntVarArgs({minus,x}),y); + if ((home.status() == SS_FAILED) || (y.min()!=-5) || (y.max()!=-2)) + return false; + } + { + TestSpace home; + IntVar x(home,0,5), q(home,1,3), y(home,2,10); + product(home,IntVarArgs({x,q}),y); + if ((home.status() == SS_FAILED) || (x.min()!=1)) + return false; + } + { + TestSpace home; + IntVar x(home,-2,2), q(home,1,3), y(home,0,0); + product(home,IntVarArgs({x,q}),y); + if ((home.status() == SS_FAILED) || !x.assigned() || (x.val()!=0)) + return false; + } + { + TestSpace home; + IntVar x(home,-5,0), q(home,2,4), y(home,-100,100); + product(home,IntVarArgs({x,q}),y); + if ((home.status() == SS_FAILED) || (y.max()!=0)) + return false; + } + { + TestSpace home; + IntVar x(home,-5,-2), q(home,-4,-2), y(home,-100,100); + product(home,IntVarArgs({x,q}),y); + if ((home.status() == SS_FAILED) || (y.min()!=4)) + return false; + } + { + TestSpace home; + const int hi=Gecode::Int::Limits::max; + IntVar minus(home,-1,-1), x(home,hi,hi), y(home,-hi,hi); + product(home,IntVarArgs({minus,x}),y); + if ((home.status() == SS_FAILED) || !y.assigned() || (y.val()!=-hi)) + return false; + } + { + CloneSpace home; + if (home.status() == SS_FAILED) + return false; + CloneSpace* clone=static_cast(home.clone()); + PropagatorGroup::all.disable(*clone); + rel(home,home.x,IRT_NQ,0); + rel(*clone,clone->x,IRT_NQ,0); + (void) home.status(); + (void) clone->status(); + rel(home,home.y,IRT_GQ,2); + rel(*clone,clone->y,IRT_GQ,2); + PropagatorGroup::all.enable(*clone); + const bool ok=(home.status()!=SS_FAILED) && + (clone->status()!=SS_FAILED) && (home.x.min()==1) && + (clone->x.min()==1) && (home.y.min()==2) && + (clone->y.min()==2); + delete clone; + if (!ok) + return false; + } + return true; + } + }; + /// Test zero-aware inverse bounds for every cofactor sign class class ProductInverseBounds : public ::Test::Base { protected: @@ -2222,6 +2329,7 @@ namespace Test { namespace Int { (void) new ProductModVarAlgebraic; (void) new ProductModVarInactive; (void) new ProductBoundsLarge; + (void) new ProductSimplifySign; (void) new ProductInverseBounds; } }; From 2ffffaa4c1a0a60964f6df40a694e80b3efbe3d4 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 15:49:27 +0200 Subject: [PATCH 16/35] Exploit product powers and aliases Zdev-Change-Id: Z63eefb11cc8b2d71f771200fd976f892416171ec432ea1e3edcdadddc501910f --- .zd/gcd/TASKS.md | 8 +- ...nd-result-alias-rewrites-for-n-ary-prod.md | 23 ++- gecode/int/arithmetic/product.hpp | 188 ++++++++++++++++-- test/int/arithmetic.cpp | 101 ++++++++++ 4 files changed, 287 insertions(+), 33 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index e099887091..98a049449d 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -4,8 +4,8 @@ - Total: 13 - Ready: 1 -- Blocked: 1 -- Done: 11 +- Blocked: 0 +- Done: 12 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -18,7 +18,7 @@ | [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | | [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | done | — | | [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | done | — | -| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | ready | gcd-009 | +| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | done | gcd-009 | | [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | | [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | done | — | -| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | blocked | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | +| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | ready | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md b/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md index 4323fabe98..f569c09b75 100644 --- a/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md +++ b/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-010" key = "product-alias-power-rewrites" area = "gcd" -status = "open" +status = "done" blocked_by = ["gcd-009"] +++ # Add repeated-factor and result-alias rewrites for n-ary product @@ -21,12 +21,12 @@ Exact product propagation recognises repeated variables as powers and result ali ## Done when -- [ ] Repeated occurrences of the same factor exploit even/odd power structure, including a nonnegative lower bound for even powers over mixed domains and sound inverse power bounds. -- [ ] The implementation does not use an intermediate-product decomposition that rejects a valid final zero merely because an earlier partial product is nonrepresentable. -- [ ] A result variable appearing among the factors is simplified using the identity result=0 or product(other factors)=1, with correct handling of multiple occurrences. -- [ ] Aliased and repeated views remain sound under cloning, propagation, reification, overflow, and zero cases. -- [ ] Focused tests demonstrate stronger pruning for squares/even powers, odd powers, repeated factors, single and multiple result aliases, signs, zeros, and reification modes. -- [ ] The affected integer library and focused Product tests build and pass. +- [x] Repeated occurrences of the same factor exploit even/odd power structure, including a nonnegative lower bound for even powers over mixed domains and sound inverse power bounds. +- [x] The implementation does not use an intermediate-product decomposition that rejects a valid final zero merely because an earlier partial product is nonrepresentable. +- [x] A result variable appearing among the factors is simplified using the identity result=0 or product(other factors)=1, with correct handling of multiple occurrences. +- [x] Aliased and repeated views remain sound under cloning, propagation, reification, overflow, and zero cases. +- [x] Focused tests demonstrate stronger pruning for squares/even powers, odd powers, repeated factors, single and multiple result aliases, signs, zeros, and reification modes. +- [x] The affected integer library and focused Product tests build and pass. ## Validation @@ -34,3 +34,12 @@ Exact product propagation recognises repeated variables as powers and result ali - Run focused Int::Arithmetic::Product alias and power tests with multiple iterations. - Run the broader Int::Arithmetic tests when available. - Run git diff --check and zd check gcd --format json. + +## Result + +Grouped repeated Product factors as powers with safe even/odd forward and inverse bounds, and added algebraic result-alias cancellation while preserving direct zero/overflow semantics. + +Validation: + +- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. +- Independent verification plus 573,300 exhaustive small power/inverse checks and Limits root checks passed after correcting ReProduct cost; diff and zd checks passed. diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index 662960dc79..91c16c3f21 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -65,6 +65,64 @@ namespace Gecode { namespace Int { namespace Arithmetic { return product_interval_mul(a,b.min,b.max); } + /// Compute a nonnegative power, saturating at the integer limit. + forceinline int + product_power_abs(int x, int n) { + const long long int a = x < 0 ? -static_cast(x) : x; + long long int p=1; + for (int i=0; i static_cast(Limits::max)/a)) + return Limits::max; + p *= a; + } + return static_cast(p); + } + + /// Bounds for a repeated occurrence viewed as an integer power. + forceinline ProductInterval + product_power_interval(IntView x, int n) { + const int l=product_power_abs(x.min(),n); + const int u=product_power_abs(x.max(),n); + if ((n & 1) != 0) { + ProductInterval p = {x.min() < 0 ? -l : l, + x.max() < 0 ? -u : u}; + return p; + } + ProductInterval p = { + ((x.min() <= 0) && (x.max() >= 0)) ? 0 : std::min(l,u), + std::max(l,u) + }; + return p; + } + + forceinline bool + product_power_le(int x, int n, int limit) { + long long int p=1; + for (int i=0; i static_cast(limit)/x)) + return false; + p *= x; + } + return true; + } + + /// Floor and ceiling of a nonnegative integer root. + inline int + product_floor_root(int x, int n) { + int l=0, u=x; + while (l < u) { + const int m=l+(u-l+1)/2; + if (product_power_le(m,n,x)) l=m; else u=m-1; + } + return l; + } + + forceinline int + product_ceil_root(int x, int n) { + if (x <= 0) return 0; + return product_floor_root(x-1,n)+1; + } + /// Compute the hull of quotients by a zero-free interval. forceinline ProductInterval product_quotient_interval(int ymin, int ymax, @@ -103,9 +161,18 @@ namespace Gecode { namespace Int { namespace Arithmetic { inline ProductInterval product_interval(const ViewArray& x, int omit=-1) { ProductInterval r = {1,1}; - for (int i=0; i= 0) { + if (y.assigned() && (y.val() == 0)) + return home.ES_SUBSUMED(*this); + ViewArray z(home,x.size()-1); + for (int i=0, j=0; i q.max)) { + GECODE_ME_CHECK(y.eq(home,0)); + return home.ES_SUBSUMED(*this); + } + } + // A nonzero result excludes a bounds-visible zero endpoint. if (!y.in(0)) for (int i=0; i(x.size()+1); - ProductInterval* suffix = r.alloc(x.size()+1); + int* representative=r.alloc(x.size()); + int* exponent=r.alloc(x.size()); + int groups=0; + for (int i=0; i(groups); + ProductInterval* prefix = r.alloc(groups+1); + ProductInterval* suffix = r.alloc(groups+1); prefix[0] = ProductInterval{1,1}; - for (int i=0; i= 0) && y.in(0)) continue; if ((q.min == 0) && (q.max == 0)) @@ -325,13 +430,52 @@ namespace Gecode { namespace Int { namespace Arithmetic { } if (!have || (d.min > d.max)) return ES_FAILED; - { - ModEvent me = x[i].gq(home,d.min); + const int n=exponent[g]; + if (n == 1) { + ModEvent me=xi.gq(home,d.min); if (me_failed(me)) return ES_FAILED; modified |= me_modified(me); - } - { - ModEvent me = x[i].lq(home,d.max); + me=xi.lq(home,d.max); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } else if ((n & 1) != 0) { + const int l = d.min < 0 ? + -product_floor_root(-d.min,n) : product_ceil_root(d.min,n); + const int u = d.max < 0 ? + -product_ceil_root(-d.max,n) : product_floor_root(d.max,n); + if (l > u) return ES_FAILED; + ModEvent me=xi.gq(home,l); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + me=xi.lq(home,u); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } else { + if (d.max < 0) return ES_FAILED; + const int lo=product_ceil_root(std::max(d.min,0),n); + const int hi=product_floor_root(d.max,n); + if (lo > hi) return ES_FAILED; + bool have_negative=xi.min() <= -lo; + bool have_positive=xi.max() >= lo; + int l=Limits::max, u=Limits::min; + if (have_negative) { + l=std::max(xi.min(),-hi); u=std::min(xi.max(),-lo); + have_negative=l <= u; + } + if (have_positive) { + const int pl=std::max(xi.min(),lo); + const int pu=std::min(xi.max(),hi); + have_positive=pl <= pu; + if (have_positive) { + if (have_negative) { l=std::min(l,pl); u=std::max(u,pu); } + else { l=pl; u=pu; } + } + } + if (!have_negative && !have_positive) return ES_FAILED; + ModEvent me=xi.gq(home,l); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + me=xi.lq(home,u); if (me_failed(me)) return ES_FAILED; modified |= me_modified(me); } @@ -421,7 +565,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline PropCost ReProduct::cost(const Space&, const ModEventDelta&) const { - return PropCost::linear(PropCost::HI,x.size()+2); + return PropCost::quadratic(PropCost::HI,x.size()+2); } template diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index e8f1891c93..cc93c7dd81 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -440,6 +440,106 @@ namespace Test { namespace Int { } }; + /// Test repeated powers and result-alias cancellation + class ProductPowerAlias : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + ProductPowerAlias(void) + : ::Test::Base("Int::Arithmetic::Product::PowerAlias") {} + virtual bool run(void) { + using namespace Gecode; + { + TestSpace home; + IntVar x(home,-10,10), y(home,-10,100); + product(home,IntVarArgs({x,x}),y); + if ((home.status() == SS_FAILED) || (y.min() != 0)) + return false; + } + { + TestSpace home; + IntVar x(home,-10,10), y(home,20,30); + product(home,IntVarArgs({x,x}),y); + if ((home.status() == SS_FAILED) || + (x.min() != -5) || (x.max() != 5)) + return false; + } + { + TestSpace home; + IntVar x(home,-10,10), y(home,20,30); + product(home,IntVarArgs({x,x,x}),y); + if ((home.status() == SS_FAILED) || !x.assigned() || + (x.val() != 3)) + return false; + } + { + TestSpace home; + IntVar x(home,-10,10), y(home,-30,-20); + product(home,IntVarArgs({x,x,x}),y); + if ((home.status() == SS_FAILED) || !x.assigned() || + (x.val() != -3)) + return false; + } + { + TestSpace home; + IntVar x(home,-10,10), two(home,2,2), y(home,50,72); + product(home,IntVarArgs({x,x,two}),y); + if ((home.status() == SS_FAILED) || + (x.min() != -6) || (x.max() != 6)) + return false; + } + { + // x*y=y has only the zero branch when x cannot be one. + TestSpace home; + IntVar x(home,2,4), y(home,-10,10); + product(home,IntVarArgs({x,y}),y); + if ((home.status() == SS_FAILED) || !y.assigned() || + (y.val() != 0)) + return false; + } + { + // A nonzero result permits cancellation of one result occurrence. + TestSpace home; + IntVar x(home,0,2), y(home,2,10); + product(home,IntVarArgs({x,y}),y); + if ((home.status() == SS_FAILED) || !x.assigned() || + (x.val() != 1)) + return false; + } + { + // Cancelling one of two result occurrences leaves y*x=1. + TestSpace home; + IntVar x(home,-1,0), y(home,-2,-1); + product(home,IntVarArgs({y,y,x}),y); + if ((home.status() == SS_FAILED) || !x.assigned() || + !y.assigned() || (x.val() != -1) || (y.val() != -1)) + return false; + } + { + // Direct n-ary evaluation retains zero after an overflowing prefix. + TestSpace home; + const int hi=Gecode::Int::Limits::max; + IntVar a(home,hi,hi), z(home,0,0), y(home,0,0); + product(home,IntVarArgs({a,a,z}),y); + if (home.status() == SS_FAILED) + return false; + } + { + TestSpace home; + IntVar x(home,-10,10), y(home,20,30); + BoolVar b(home,1,1); + product(home,IntVarArgs({x,x}),y,Reify(b,RM_EQV)); + if ((home.status() == SS_FAILED) || + (x.min() != -5) || (x.max() != 5)) + return false; + } + return true; + } + }; + /// Test zero-aware inverse bounds for every cofactor sign class class ProductInverseBounds : public ::Test::Base { protected: @@ -2330,6 +2430,7 @@ namespace Test { namespace Int { (void) new ProductModVarInactive; (void) new ProductBoundsLarge; (void) new ProductSimplifySign; + (void) new ProductPowerAlias; (void) new ProductInverseBounds; } }; From 566232682ace038750b20b5e35587c8c4c726278 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 12 Aug 2026 16:11:07 +0200 Subject: [PATCH 17/35] Remove propagator support enumeration Zdev-Change-Id: Z577863e37baab23e1d34fbb66cf6d17c1a1caba746ac1aa2fb7582138a82594a --- .zd/gcd/TASKS.md | 6 +- ...meration-from-number-theoretic-propagat.md | 25 ++- gecode/int/arithmetic.hh | 40 ++--- gecode/int/arithmetic/divides.hpp | 82 +++------- gecode/int/arithmetic/gcd.hpp | 145 +++++------------- gecode/int/arithmetic/product-mod.hpp | 40 +++-- gecode/int/arithmetic/product.hpp | 14 +- test/int/arithmetic.cpp | 119 ++++++++++++-- 8 files changed, 234 insertions(+), 237 deletions(-) diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md index 98a049449d..cc1a76b475 100644 --- a/.zd/gcd/TASKS.md +++ b/.zd/gcd/TASKS.md @@ -3,9 +3,9 @@ # Tasks: gcd - Total: 13 -- Ready: 1 +- Ready: 0 - Blocked: 0 -- Done: 12 +- Done: 13 | ID | Task | State | Blocked by | | --- | --- | --- | --- | @@ -21,4 +21,4 @@ | [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | done | gcd-009 | | [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | | [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | done | — | -| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | ready | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | +| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | done | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md index d9c697e2eb..2c237fd42c 100644 --- a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md +++ b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md @@ -3,7 +3,7 @@ schema_version = 1 id = "gcd-013" key = "eliminate-support-enumeration" area = "gcd" -status = "open" +status = "done" blocked_by = ["gcd-008", "gcd-009", "gcd-010", "gcd-011", "gcd-012"] +++ # Eliminate support enumeration from number-theoretic propagators @@ -22,13 +22,13 @@ Every propagator added in the gcd area uses bounds or algebraic reasoning instea ## Done when -- [ ] No audited propagator or helper enumerates a Cartesian product of variable domains, allocates tuple-support tables, or filters domains by projected tuple supports. -- [ ] Gcd/ReGcd and ReDivides use bounds and number-theoretic identities for propagation and conservative algebraic reification status, with no small-domain enumeration fallback. -- [ ] Ordinary propagation is expressed through sound bounds, sign/zero classification, divisibility, congruence, or algebraic rewrites, with conservative behavior where those deductions are inconclusive. -- [ ] Reified entailment and disentailment use sound bounds or algebraic tests and exact evaluation only for fully assigned relations; negative or unfixed cases wait conservatively instead of enumerating supports. -- [ ] Propagation conditions, subscriptions, and costs match the information actually used, including bounds subscriptions where interior-domain events are no longer required. -- [ ] Focused tests exercise GCD, divides, product, fixed-modulus product_mod, and variable-modulus product_mod on domains above the former 200,000-tuple cutoff, relevant reification modes, and fully assigned true and false leaves. -- [ ] The affected integer library, all focused new-propagator tests with multiple iterations, and the broader integer arithmetic suite build and pass. +- [x] No audited propagator or helper enumerates a Cartesian product of variable domains, allocates tuple-support tables, or filters domains by projected tuple supports. +- [x] Gcd/ReGcd and ReDivides use bounds and number-theoretic identities for propagation and conservative algebraic reification status, with no small-domain enumeration fallback. +- [x] Ordinary propagation is expressed through sound bounds, sign/zero classification, divisibility, congruence, or algebraic rewrites, with conservative behavior where those deductions are inconclusive. +- [x] Reified entailment and disentailment use sound bounds or algebraic tests and exact evaluation only for fully assigned relations; negative or unfixed cases wait conservatively instead of enumerating supports. +- [x] Propagation conditions, subscriptions, and costs match the information actually used, including bounds subscriptions where interior-domain events are no longer required. +- [x] Focused tests exercise GCD, divides, product, fixed-modulus product_mod, and variable-modulus product_mod on domains above the former 200,000-tuple cutoff, relevant reification modes, and fully assigned true and false leaves. +- [x] The affected integer library, all focused new-propagator tests with multiple iterations, and the broader integer arithmetic suite build and pass. ## Validation @@ -37,3 +37,12 @@ Every propagator added in the gcd area uses bounds or algebraic reasoning instea - Run focused Gcd, Divides, Product, ProductMod, and ProductModVar tests with multiple iterations. - Run the broader Int::Arithmetic tests. - Run git diff --check and zd check gcd --format json. + +## Result + +Removed all Cartesian/support enumeration and tuple cutoffs from GCD and divides, converted audited actors to bounds/algebraic conservative propagation with weak lifecycle where needed, and added above-cutoff staged leaf coverage for every new family. + +Validation: + +- Built gecodeint_shared and gecode-test; Gcd, Divides, Product, ProductMod, ProductModVar and large-leaf tests passed for three iterations; broader Int::Arithmetic passed. +- Independent spec/standards verification and static anti-enumeration audit passed; git diff --check and zd check passed. diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 2c8a1c7300..fe4ecdc400 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -769,16 +769,16 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { /** - * \brief Domain propagator for \f$\gcd(x_0,x_1)=x_2\f$ + * \brief Bounds propagator for \f$\gcd(x_0,x_1)=x_2\f$ * * Requires \code #include \endcode * \ingroup FuncIntProp */ - class Gcd : public TernaryPropagator { + class Gcd : public TernaryPropagator { protected: - using TernaryPropagator::x0; - using TernaryPropagator::x1; - using TernaryPropagator::x2; + using TernaryPropagator::x0; + using TernaryPropagator::x1; + using TernaryPropagator::x2; /// Constructor for cloning \a p Gcd(Space& home, Gcd& p); /// Constructor for posting @@ -794,7 +794,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual ExecStatus propagate(Space& home, const ModEventDelta& med); }; - /** \brief Reified domain propagator for \f$\gcd(x_0,x_1)=x_2\f$ */ + /** \brief Reified bounds propagator for \f$\gcd(x_0,x_1)=x_2\f$ */ template class ReGcd : public Propagator { protected: @@ -827,7 +827,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { /** - * \brief Reified domain propagator for divisibility + * \brief Reified bounds propagator for divisibility * * The relation is \f$\exists k\in\mathbb Z:x_1=x_0k\f$. * Requires \code #include \endcode @@ -835,11 +835,11 @@ namespace Gecode { namespace Int { namespace Arithmetic { */ template class ReDivides : - public ReBinaryPropagator { + public ReBinaryPropagator { protected: - using ReBinaryPropagator::x0; - using ReBinaryPropagator::x1; - using ReBinaryPropagator::b; + using ReBinaryPropagator::x0; + using ReBinaryPropagator::x1; + using ReBinaryPropagator::b; /// Constructor for posting ReDivides(Home home, IntView x0, IntView x1, BoolView b); /// Constructor for cloning \a p @@ -853,6 +853,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual PropCost cost(const Space& home, const ModEventDelta& med) const; /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size + virtual size_t dispose(Space& home); }; }}} @@ -880,7 +882,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual size_t dispose(Space& home); }; - /** \brief Reified domain propagator for an exact n-ary product + /** \brief Reified bounds propagator for an exact n-ary product * \ingroup FuncIntProp */ template @@ -907,13 +909,13 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { - /** \brief Domain propagator for a fixed-modulus n-ary product + /** \brief Bounds propagator for a fixed-modulus n-ary product * \ingroup FuncIntProp */ - class ProductMod : public NaryOnePropagator { + class ProductMod : public NaryOnePropagator { protected: - using NaryOnePropagator::x; - using NaryOnePropagator::y; + using NaryOnePropagator::x; + using NaryOnePropagator::y; int m; ProductMod(Home home, ViewArray& x, int m, IntView y); ProductMod(Space& home, ProductMod& p); @@ -924,7 +926,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual ExecStatus propagate(Space& home, const ModEventDelta& med); }; - /** \brief Domain propagator for a variable-modulus n-ary product + /** \brief Bounds propagator for a variable-modulus n-ary product * \ingroup FuncIntProp */ class ProductModVar : public Propagator { @@ -944,7 +946,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual size_t dispose(Space& home); }; - /** \brief Reified domain propagator for a variable-modulus product + /** \brief Reified bounds propagator for a variable-modulus product * \ingroup FuncIntProp */ template @@ -967,7 +969,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual size_t dispose(Space& home); }; - /** \brief Reified domain propagator for a fixed-modulus n-ary product + /** \brief Reified bounds propagator for a fixed-modulus n-ary product * \ingroup FuncIntProp */ template diff --git a/gecode/int/arithmetic/divides.hpp b/gecode/int/arithmetic/divides.hpp index 256cd3c368..7364649a4a 100644 --- a/gecode/int/arithmetic/divides.hpp +++ b/gecode/int/arithmetic/divides.hpp @@ -35,20 +35,11 @@ namespace Gecode { namespace Int { namespace Arithmetic { - /// Largest Cartesian product enumerated for domain propagation. - const unsigned long long divides_support_limit = 200000ULL; - forceinline bool divides_value(int divisor, int dividend) { return (divisor == 0) ? (dividend == 0) : (dividend % divisor == 0); } - forceinline bool - divides_enumerable(const IntView& x0, const IntView& x1) { - return (static_cast(x0.size()) * x1.size()) <= - divides_support_limit; - } - inline RelTest divides_status(const IntView& x0, const IntView& x1) { if (x0 == x1) @@ -61,58 +52,15 @@ namespace Gecode { namespace Int { namespace Arithmetic { return RT_FALSE; if (x0.assigned() && x1.assigned()) return divides_value(x0.val(),x1.val()) ? RT_TRUE : RT_FALSE; - if (!divides_enumerable(x0,x1)) - return RT_MAYBE; - bool has_true = false, has_false = false; - for (ViewValues i0(x0); i0(); ++i0) - for (ViewValues i1(x1); i1(); ++i1) { - if (divides_value(i0.val(),i1.val())) - has_true = true; - else - has_false = true; - if (has_true && has_false) - return RT_MAYBE; - } - return has_true ? RT_TRUE : RT_FALSE; - } - - /// Keep precisely values with support in the relation (or its negation). - inline ExecStatus - divides_filter(Space& home, IntView x0, IntView x1, bool positive) { - if (x0 == x1) - return positive ? ES_OK : ES_FAILED; - if (!divides_enumerable(x0,x1)) - return ES_OK; - - Region r; - unsigned int cap = x0.size() * x1.size(); - int* s0 = r.alloc(cap); - int* s1 = r.alloc(cap); - unsigned int n = 0; - for (ViewValues i0(x0); i0(); ++i0) - for (ViewValues i1(x1); i1(); ++i1) - if (divides_value(i0.val(),i1.val()) == positive) { - s0[n] = i0.val(); s1[n] = i1.val(); n++; - } - if (n == 0) - return ES_FAILED; - std::sort(s0,s0+n); std::sort(s1,s1+n); - unsigned int n0=1, n1=1; - for (unsigned int i=1; i forceinline ReDivides::ReDivides(Home home, IntView y0, IntView y1, BoolView b0) - : ReBinaryPropagator(home,y0,y1,b0) {} + : ReBinaryPropagator(home,y0,y1,b0) { + home.notice(*this,AP_WEAKLY); + } template inline ExecStatus @@ -139,7 +87,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline ReDivides::ReDivides(Space& home, ReDivides& p) - : ReBinaryPropagator(home,p) {} + : ReBinaryPropagator(home,p) {} template forceinline Actor* @@ -150,7 +98,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline PropCost ReDivides::cost(const Space&, const ModEventDelta&) const { - return PropCost::binary(PropCost::HI); + return PropCost::binary(PropCost::LO); } template @@ -159,16 +107,18 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (b.one()) { if (rm == RM_PMI) return home.ES_SUBSUMED(*this); - GECODE_ES_CHECK(divides_filter(home,x0,x1,true)); - if (divides_status(x0,x1) == RT_TRUE) + const RelTest rt=divides_status(x0,x1); + if (rt == RT_FALSE) return ES_FAILED; + if (rt == RT_TRUE) return home.ES_SUBSUMED(*this); return ES_FIX; } if (b.zero()) { if (rm == RM_IMP) return home.ES_SUBSUMED(*this); - GECODE_ES_CHECK(divides_filter(home,x0,x1,false)); - if (divides_status(x0,x1) == RT_FALSE) + const RelTest rt=divides_status(x0,x1); + if (rt == RT_TRUE) return ES_FAILED; + if (rt == RT_FALSE) return home.ES_SUBSUMED(*this); return ES_FIX; } @@ -189,4 +139,12 @@ namespace Gecode { namespace Int { namespace Arithmetic { return home.ES_SUBSUMED(*this); } + template + forceinline size_t + ReDivides::dispose(Space& home) { + home.ignore(*this,AP_WEAKLY); + (void) ReBinaryPropagator::dispose(home); + return sizeof(*this); + } + }}} diff --git a/gecode/int/arithmetic/gcd.hpp b/gecode/int/arithmetic/gcd.hpp index b7de5c0c11..ad07f227a7 100644 --- a/gecode/int/arithmetic/gcd.hpp +++ b/gecode/int/arithmetic/gcd.hpp @@ -35,9 +35,6 @@ namespace Gecode { namespace Int { namespace Arithmetic { - /// Largest Cartesian product enumerated for domain propagation. - const unsigned long long gcd_support_limit = 200000ULL; - forceinline int gcd_value(int a, int b) { unsigned int x = static_cast((a < 0) ? -a : a); @@ -55,85 +52,28 @@ namespace Gecode { namespace Int { namespace Arithmetic { (x.max() < 0) ? -x.max() : x.max()); } - forceinline bool - gcd_alias_ok(const IntView& x0, int a, const IntView& x1, int b, - const IntView& x2, int c) { - return ((x0 != x1) || (a == b)) && - ((x0 != x2) || (a == c)) && - ((x1 != x2) || (b == c)); - } - - forceinline bool - gcd_enumerable(const IntView& x0, const IntView& x1, const IntView& x2) { - unsigned long long n = x0.size(); - n *= x1.size(); - if (n > gcd_support_limit) - return false; - n *= x2.size(); - return n <= gcd_support_limit; - } - - /// Determine whether satisfying and violating assignments exist. - inline void - gcd_status(const IntView& x0, const IntView& x1, const IntView& x2, - bool& has_true, bool& has_false) { - has_true = has_false = false; - for (ViewValues i0(x0); i0(); ++i0) - for (ViewValues i1(x1); i1(); ++i1) - for (ViewValues i2(x2); i2(); ++i2) { - int a=i0.val(), b=i1.val(), c=i2.val(); - if (!gcd_alias_ok(x0,a,x1,b,x2,c)) - continue; - if (gcd_value(a,b) == c) - has_true = true; - else - has_false = true; - if (has_true && has_false) - return; - } - } - - /// Keep precisely the values supported by either the relation or its negation. - inline ExecStatus - gcd_filter(Space& home, IntView x0, IntView x1, IntView x2, bool positive) { - Region r; - unsigned int cap = static_cast( - static_cast(x0.size()) * x1.size() * x2.size()); - int* s0 = r.alloc(cap); - int* s1 = r.alloc(cap); - int* s2 = r.alloc(cap); - unsigned int n = 0; - for (ViewValues i0(x0); i0(); ++i0) - for (ViewValues i1(x1); i1(); ++i1) - for (ViewValues i2(x2); i2(); ++i2) { - int a=i0.val(), b=i1.val(), c=i2.val(); - if (!gcd_alias_ok(x0,a,x1,b,x2,c)) - continue; - if ((gcd_value(a,b) == c) == positive) { - s0[n]=a; s1[n]=b; s2[n]=c; n++; - } - } - if (n == 0) - return ES_FAILED; - std::sort(s0,s0+n); std::sort(s1,s1+n); std::sort(s2,s2+n); - unsigned int n0=1, n1=1, n2=1; - for (unsigned int i=1; i std::max(gcd_max_abs(x0),gcd_max_abs(x1))) + return RT_FALSE; + if (x0 == x1) { + if (x0.assigned() && x2.assigned()) + return gcd_value(x0.val(),x0.val()) == x2.val() ? RT_TRUE : RT_FALSE; + return RT_MAYBE; } - Iter::Values::Array i0(s0,n0); - GECODE_ME_CHECK(x0.inter_v(home,i0,false)); - Iter::Values::Array i1(s1,n1); - GECODE_ME_CHECK(x1.inter_v(home,i1,false)); - Iter::Values::Array i2(s2,n2); - GECODE_ME_CHECK(x2.inter_v(home,i2,false)); - return ES_OK; + if (x0.assigned() && x1.assigned()) { + const int g=gcd_value(x0.val(),x1.val()); + if (!x2.in(g)) return RT_FALSE; + return x2.assigned() ? RT_TRUE : RT_MAYBE; + } + return RT_MAYBE; } forceinline Gcd::Gcd(Home home, IntView y0, IntView y1, IntView y2) - : TernaryPropagator(home,y0,y1,y2) {} + : TernaryPropagator(home,y0,y1,y2) {} inline ExecStatus Gcd::post(Home home, IntView x0, IntView x1, IntView x2) { @@ -149,7 +89,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline Gcd::Gcd(Space& home, Gcd& p) - : TernaryPropagator(home,p) {} + : TernaryPropagator(home,p) {} forceinline Actor* Gcd::copy(Space& home) { @@ -158,7 +98,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline PropCost Gcd::cost(const Space&, const ModEventDelta&) const { - return PropCost::ternary(PropCost::HI); + return PropCost::ternary(PropCost::LO); } inline ExecStatus @@ -169,10 +109,6 @@ namespace Gecode { namespace Int { namespace Arithmetic { GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); return home.ES_SUBSUMED(*this); } - if (gcd_enumerable(x0,x1,x2)) - GECODE_ES_CHECK(gcd_filter(home,x0,x1,x2,true)); - if (x0.assigned() && x1.assigned() && x2.assigned()) - return home.ES_SUBSUMED(*this); return ES_FIX; } @@ -180,9 +116,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline ReGcd::ReGcd(Home home, IntView y0, IntView y1, IntView y2, BoolView b0) : Propagator(home), x0(y0), x1(y1), x2(y2), b(b0) { - x0.subscribe(home,*this,PC_INT_DOM); - x1.subscribe(home,*this,PC_INT_DOM); - x2.subscribe(home,*this,PC_INT_DOM); + home.notice(*this,AP_WEAKLY); + x0.subscribe(home,*this,PC_INT_BND); + x1.subscribe(home,*this,PC_INT_BND); + x2.subscribe(home,*this,PC_INT_BND); b.subscribe(home,*this,PC_BOOL_VAL); } @@ -217,15 +154,15 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline PropCost ReGcd::cost(const Space&, const ModEventDelta&) const { - return PropCost::ternary(PropCost::HI); + return PropCost::ternary(PropCost::LO); } template forceinline void ReGcd::reschedule(Space& home) { - x0.reschedule(home,*this,PC_INT_DOM); - x1.reschedule(home,*this,PC_INT_DOM); - x2.reschedule(home,*this,PC_INT_DOM); + x0.reschedule(home,*this,PC_INT_BND); + x1.reschedule(home,*this,PC_INT_BND); + x2.reschedule(home,*this,PC_INT_BND); b.reschedule(home,*this,PC_BOOL_VAL); } @@ -240,37 +177,35 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (b.zero()) { if (rm == RM_IMP) return home.ES_SUBSUMED(*this); - if (gcd_enumerable(x0,x1,x2)) - GECODE_ES_CHECK(gcd_filter(home,x0,x1,x2,false)); - if (x0.assigned() && x1.assigned() && x2.assigned()) - return home.ES_SUBSUMED(*this); + const RelTest rt=gcd_status(x0,x1,x2); + if (rt == RT_TRUE) return ES_FAILED; + if (rt == RT_FALSE) return home.ES_SUBSUMED(*this); return ES_FIX; } - bool has_true, has_false; - if (gcd_enumerable(x0,x1,x2)) { - gcd_status(x0,x1,x2,has_true,has_false); - if (!has_true) { + switch (gcd_status(x0,x1,x2)) { + case RT_FALSE: if (rm != RM_PMI) GECODE_ME_CHECK(b.zero(home)); return home.ES_SUBSUMED(*this); - } - if (!has_false) { + case RT_TRUE: if (rm != RM_IMP) GECODE_ME_CHECK(b.one(home)); return home.ES_SUBSUMED(*this); - } + case RT_MAYBE: + return ES_FIX; + default: GECODE_NEVER; } - return ES_FIX; } template forceinline size_t ReGcd::dispose(Space& home) { - x0.cancel(home,*this,PC_INT_DOM); - x1.cancel(home,*this,PC_INT_DOM); - x2.cancel(home,*this,PC_INT_DOM); + x0.cancel(home,*this,PC_INT_BND); + x1.cancel(home,*this,PC_INT_BND); + x2.cancel(home,*this,PC_INT_BND); b.cancel(home,*this,PC_BOOL_VAL); + home.ignore(*this,AP_WEAKLY); (void) Propagator::dispose(home); return sizeof(*this); } diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 0fbe4d7405..39d113f882 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -148,7 +148,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline ProductMod::ProductMod(Home home, ViewArray& z, int modulus, IntView w) - : NaryOnePropagator(home,z,w), m(modulus) {} + : NaryOnePropagator(home,z,w), m(modulus) {} inline ExecStatus ProductMod::post(Home home, ViewArray& x, int m, IntView y) { @@ -177,7 +177,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline ProductMod::ProductMod(Space& home, ProductMod& p) - : NaryOnePropagator(home,p), m(p.m) {} + : NaryOnePropagator(home,p), m(p.m) {} forceinline Actor* ProductMod::copy(Space& home) { @@ -434,9 +434,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { ProductModVar::ProductModVar(Home home, ViewArray& z, IntView modulus, IntView w) : Propagator(home), x(z), m(modulus), y(w) { + home.notice(*this,AP_WEAKLY); x.subscribe(home,*this,PC_INT_VAL); m.subscribe(home,*this,PC_INT_BND); - y.subscribe(home,*this,PC_INT_DOM); + y.subscribe(home,*this,PC_INT_BND); } inline ExecStatus @@ -487,7 +488,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { ProductModVar::reschedule(Space& home) { x.reschedule(home,*this,PC_INT_VAL); m.reschedule(home,*this,PC_INT_BND); - y.reschedule(home,*this,PC_INT_DOM); + y.reschedule(home,*this,PC_INT_BND); } inline ExecStatus @@ -558,7 +559,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { ProductModVar::dispose(Space& home) { x.cancel(home,*this,PC_INT_VAL); m.cancel(home,*this,PC_INT_BND); - y.cancel(home,*this,PC_INT_DOM); + y.cancel(home,*this,PC_INT_BND); + home.ignore(*this,AP_WEAKLY); (void) Propagator::dispose(home); return sizeof(*this); } @@ -568,9 +570,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { ReProductModVar::ReProductModVar(Home home, ViewArray& z, IntView modulus, IntView w, BoolView c) : Propagator(home), x(z), m(modulus), y(w), b(c) { + home.notice(*this,AP_WEAKLY); x.subscribe(home,*this,PC_INT_VAL); - m.subscribe(home,*this,PC_INT_DOM); - y.subscribe(home,*this,PC_INT_DOM); + m.subscribe(home,*this,PC_INT_BND); + y.subscribe(home,*this,PC_INT_BND); b.subscribe(home,*this,PC_BOOL_VAL); } @@ -613,8 +616,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline void ReProductModVar::reschedule(Space& home) { x.reschedule(home,*this,PC_INT_VAL); - m.reschedule(home,*this,PC_INT_DOM); - y.reschedule(home,*this,PC_INT_DOM); + m.reschedule(home,*this,PC_INT_BND); + y.reschedule(home,*this,PC_INT_BND); b.reschedule(home,*this,PC_BOOL_VAL); } @@ -655,9 +658,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline size_t ReProductModVar::dispose(Space& home) { x.cancel(home,*this,PC_INT_VAL); - m.cancel(home,*this,PC_INT_DOM); - y.cancel(home,*this,PC_INT_DOM); + m.cancel(home,*this,PC_INT_BND); + y.cancel(home,*this,PC_INT_BND); b.cancel(home,*this,PC_BOOL_VAL); + home.ignore(*this,AP_WEAKLY); (void) Propagator::dispose(home); return sizeof(*this); } @@ -667,8 +671,9 @@ namespace Gecode { namespace Int { namespace Arithmetic { ReProductMod::ReProductMod(Home home, ViewArray& z, int modulus, IntView w, BoolView c) : Propagator(home), x(z), y(w), b(c), m(modulus) { - x.subscribe(home,*this,PC_INT_DOM); - y.subscribe(home,*this,PC_INT_DOM); + home.notice(*this,AP_WEAKLY); + x.subscribe(home,*this,PC_INT_BND); + y.subscribe(home,*this,PC_INT_BND); b.subscribe(home,*this,PC_BOOL_VAL); } @@ -737,8 +742,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline void ReProductMod::reschedule(Space& home) { - x.reschedule(home,*this,PC_INT_DOM); - y.reschedule(home,*this,PC_INT_DOM); + x.reschedule(home,*this,PC_INT_BND); + y.reschedule(home,*this,PC_INT_BND); b.reschedule(home,*this,PC_BOOL_VAL); } @@ -776,9 +781,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline size_t ReProductMod::dispose(Space& home) { - x.cancel(home,*this,PC_INT_DOM); - y.cancel(home,*this,PC_INT_DOM); + x.cancel(home,*this,PC_INT_BND); + y.cancel(home,*this,PC_INT_BND); b.cancel(home,*this,PC_BOOL_VAL); + home.ignore(*this,AP_WEAKLY); (void) Propagator::dispose(home); return sizeof(*this); } diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index 91c16c3f21..6fdcde3333 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -518,8 +518,9 @@ namespace Gecode { namespace Int { namespace Arithmetic { ReProduct::ReProduct(Home home, ViewArray& z, IntView w, BoolView c) : Propagator(home), x(z), y(w), b(c) { - x.subscribe(home,*this,PC_INT_DOM); - y.subscribe(home,*this,PC_INT_DOM); + home.notice(*this,AP_WEAKLY); + x.subscribe(home,*this,PC_INT_BND); + y.subscribe(home,*this,PC_INT_BND); b.subscribe(home,*this,PC_BOOL_VAL); } @@ -571,8 +572,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline void ReProduct::reschedule(Space& home) { - x.reschedule(home,*this,PC_INT_DOM); - y.reschedule(home,*this,PC_INT_DOM); + x.reschedule(home,*this,PC_INT_BND); + y.reschedule(home,*this,PC_INT_BND); b.reschedule(home,*this,PC_BOOL_VAL); } @@ -608,9 +609,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { template forceinline size_t ReProduct::dispose(Space& home) { - x.cancel(home,*this,PC_INT_DOM); - y.cancel(home,*this,PC_INT_DOM); + x.cancel(home,*this,PC_INT_BND); + y.cancel(home,*this,PC_INT_BND); b.cancel(home,*this,PC_BOOL_VAL); + home.ignore(*this,AP_WEAKLY); (void) Propagator::dispose(home); return sizeof(*this); } diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index cc93c7dd81..b3676cb17a 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -65,7 +65,9 @@ namespace Test { namespace Int { /// Create and register test GcdXYZ(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XYZ::"+str(ipl)+"::"+s,3,d,true,ipl) {} + : Test("Arithmetic::Gcd::XYZ::"+str(ipl)+"::"+s,3,d,true,ipl) { + contest=CTL_NONE; testfix=false; + } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { return gcd_value(x[0],x[1]) == x[2]; @@ -87,7 +89,9 @@ namespace Test { namespace Int { /// Create and register test GcdXXY(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XXY::"+str(ipl)+"::"+s,2,d,true,ipl) {} + : Test("Arithmetic::Gcd::XXY::"+str(ipl)+"::"+s,2,d,true,ipl) { + contest=CTL_NONE; testfix=false; + } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { return gcd_value(x[0],x[0]) == x[1]; @@ -109,7 +113,9 @@ namespace Test { namespace Int { /// Create and register test GcdXYX(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XYX::"+str(ipl)+"::"+s,2,d,true,ipl) {} + : Test("Arithmetic::Gcd::XYX::"+str(ipl)+"::"+s,2,d,true,ipl) { + contest=CTL_NONE; testfix=false; + } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { return gcd_value(x[0],x[1]) == x[0]; @@ -131,7 +137,9 @@ namespace Test { namespace Int { /// Create and register test GcdXXX(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XXX::"+str(ipl)+"::"+s,1,d,true,ipl) {} + : Test("Arithmetic::Gcd::XXX::"+str(ipl)+"::"+s,1,d,true,ipl) { + contest=CTL_NONE; testfix=false; + } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { return gcd_value(x[0],x[0]) == x[0]; @@ -154,7 +162,7 @@ namespace Test { namespace Int { DividesXY(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Divides::XY::"+str(ipl)+"::"+s, - 2,d,true,ipl) {} + 2,d,true,ipl) { contest=CTL_NONE; testfix=false; } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { return (x[0] == 0) ? (x[1] == 0) : (x[1] % x[0] == 0); @@ -178,7 +186,7 @@ namespace Test { namespace Int { DividesXX(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Divides::XX::"+str(ipl)+"::"+s, - 1,d,true,ipl) {} + 1,d,true,ipl) { contest=CTL_NONE; testfix=false; } /// Every integer divides itself, including zero virtual bool solution(const Assignment&) const { return true; @@ -622,7 +630,7 @@ namespace Test { namespace Int { ProductModXYZR(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::XYZR::"+str(ipl)+"::"+s, - 4,d,true,ipl), m(m0) { contest=CTL_NONE; } + 4,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { return product_mod_value(x,3,m) == x[3]; } @@ -645,7 +653,7 @@ namespace Test { namespace Int { ProductModEmpty(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::Empty::"+str(ipl)+"::"+s, - 1,d,true,ipl), m(m0) { contest=CTL_NONE; } + 1,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { return x[0] == (1 % m); } @@ -666,7 +674,7 @@ namespace Test { namespace Int { ProductModSingleton(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::Singleton::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) { contest=CTL_NONE; } + 2,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { int r = x[0] % m; if (r < 0) @@ -690,7 +698,7 @@ namespace Test { namespace Int { ProductModXXYAlias(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::XXYAlias::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) { contest=CTL_NONE; } + 2,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { long long int a = x[0] % m; long long int b = x[1] % m; @@ -810,7 +818,7 @@ namespace Test { namespace Int { ProductModVarXYMR(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::XYMR::"+str(ipl)+"::"+s, - 4,d,true,ipl) { contest=CTL_NONE; } + 4,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { return (x[2] > 0) && (product_mod_value(x,2,x[2]) == x[3]); @@ -832,7 +840,7 @@ namespace Test { namespace Int { ProductModVarEmpty(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Empty::"+str(ipl)+"::"+s, - 2,d,true,ipl) { contest=CTL_NONE; } + 2,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == (1 % x[0])); } @@ -851,7 +859,7 @@ namespace Test { namespace Int { ProductModVarSingleton(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Singleton::"+str(ipl)+"::"+s, - 3,d,true,ipl) { contest=CTL_NONE; } + 3,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -877,7 +885,7 @@ namespace Test { namespace Int { ProductModVarRepeated(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::Repeated::"+str(ipl)+"::"+s, - 3,d,true,ipl) { contest=CTL_NONE; } + 3,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -903,7 +911,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModFactorAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { return (x[0] > 0) && (x[1] == 0); } @@ -925,7 +933,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::FactorResultAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment& x) const { if (x[1] <= 0) return false; @@ -951,7 +959,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModResultAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; testfix=false; } virtual bool solution(const Assignment&) const { return false; } virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), @@ -1158,6 +1166,82 @@ namespace Test { namespace Int { } }; + /// Staged assigned leaves above the former Cartesian support cutoff + class ArithmeticLargeLeaves : public ::Test::Base { + protected: + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + static bool control(const Gecode::BoolVar& b, Gecode::ReifyMode rm, + bool truth) { + if (truth && (rm != Gecode::RM_IMP)) + return b.assigned() && (b.val() == 1); + if (!truth && (rm != Gecode::RM_PMI)) + return b.assigned() && (b.val() == 0); + return true; + } + public: + ArithmeticLargeLeaves(void) + : ::Test::Base("Int::Arithmetic::LargeAssignedLeaves") {} + virtual bool run(void) { + using namespace Gecode; + const ReifyMode rms[] = {RM_EQV,RM_IMP,RM_PMI}; + for (unsigned int r=0; r<3; r++) + for (int truth=0; truth<=1; truth++) { + { + TestSpace home; + IntVar a(home,1,1000), c(home,1,1000), g(home,0,1000); + BoolVar b(home,0,1); + gcd(home,a,c,g,Reify(b,rms[r])); + rel(home,a,IRT_EQ,84); rel(home,c,IRT_EQ,30); + rel(home,g,IRT_EQ,truth ? 6 : 7); + if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) + return false; + } + { + TestSpace home; + IntVar d(home,1,1000), n(home,1,1000); BoolVar b(home,0,1); + divides(home,d,n,Reify(b,rms[r])); + rel(home,d,IRT_EQ,truth ? 7 : 8); rel(home,n,IRT_EQ,84); + if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) + return false; + } + { + TestSpace home; + IntVarArgs x(home,3,1,1000); IntVar y(home,1,1000000000); + BoolVar b(home,0,1); product(home,x,y,Reify(b,rms[r])); + rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); + rel(home,x[2],IRT_EQ,5); rel(home,y,IRT_EQ,truth ? 30 : 31); + if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) + return false; + } + { + TestSpace home; + IntVarArgs x(home,3,1,1000); IntVar y(home,0,96); + BoolVar b(home,0,1); + product_mod(home,x,97,y,Reify(b,rms[r])); + rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); + rel(home,x[2],IRT_EQ,5); rel(home,y,IRT_EQ,truth ? 30 : 31); + if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) + return false; + } + { + TestSpace home; + IntVarArgs x(home,3,1,1000); IntVar m(home,1,1000); + IntVar y(home,0,999); BoolVar b(home,0,1); + product_mod(home,x,m,y,Reify(b,rms[r])); + rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); + rel(home,x[2],IRT_EQ,5); rel(home,m,IRT_EQ,97); + rel(home,y,IRT_EQ,truth ? 30 : 31); + if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) + return false; + } + } + return true; + } + }; + /// %Test that a nonpositive modular-product modulus is rejected class ProductModInvalidModulus : public ::Test::Base { protected: @@ -2428,6 +2512,7 @@ namespace Test { namespace Int { (void) new ProductModVarBounds; (void) new ProductModVarAlgebraic; (void) new ProductModVarInactive; + (void) new ArithmeticLargeLeaves; (void) new ProductBoundsLarge; (void) new ProductSimplifySign; (void) new ProductPowerAlias; From f7ebd89db0077ec94077417bded06bf0a1f8f02c Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Sat, 15 Aug 2026 23:34:53 +0200 Subject: [PATCH 18/35] Correct GCD propagation documentation --- gecode/int.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gecode/int.hh b/gecode/int.hh index b969e00727..59a4fb23ae 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3035,8 +3035,7 @@ namespace Gecode { * * The greatest common divisor is nonnegative, with * \f$\gcd(0,0)=0\f$. Negative operands are interpreted by absolute - * value. Supports domain consistency when domains are sufficiently small - * and sound bounds propagation otherwise. + * value. Supports sound bounds propagation. */ GECODE_INT_EXPORT void gcd(Home home, IntVar x0, IntVar x1, IntVar x2, From bf109af672467e4257ed8931b6151cfa42b7d7f7 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Mon, 24 Aug 2026 16:38:34 +0200 Subject: [PATCH 19/35] Strengthen enumeration-free number theory propagation --- gecode/int/arithmetic/divides.hpp | 80 +++++++++++++- gecode/int/arithmetic/gcd.hpp | 145 ++++++++++++++++++++++++-- gecode/int/arithmetic/product-mod.hpp | 56 +++++----- test/int/arithmetic.cpp | 4 +- 4 files changed, 251 insertions(+), 34 deletions(-) diff --git a/gecode/int/arithmetic/divides.hpp b/gecode/int/arithmetic/divides.hpp index 7364649a4a..5d1369f451 100644 --- a/gecode/int/arithmetic/divides.hpp +++ b/gecode/int/arithmetic/divides.hpp @@ -40,6 +40,66 @@ namespace Gecode { namespace Int { namespace Arithmetic { return (divisor == 0) ? (dividend == 0) : (dividend % divisor == 0); } + /// Tighten a dividend to the extreme multiples of a nonzero divisor. + inline ExecStatus + divides_multiple_bounds(Home home, IntView dividend, int divisor) { + const int a=(divisor < 0) ? -divisor : divisor; + assert(a > 0); + const long long int l = + ceil_div_xx(static_cast(dividend.min()), + static_cast(a)) * a; + const long long int u = + floor_div_xx(static_cast(dividend.max()), + static_cast(a)) * a; + if (l > u) + return ES_FAILED; + GECODE_ME_CHECK(dividend.gq(home,static_cast(l))); + GECODE_ME_CHECK(dividend.lq(home,static_cast(u))); + return ES_OK; + } + + /// Enforce divisibility with bounds reasoning only. + inline ExecStatus + divides_bnd(Space& home, IntView divisor, IntView dividend) { + if (divisor == dividend) + return ES_OK; + if (dividend.assigned() && (dividend.val() == 0)) + return ES_OK; + if (divisor.assigned()) { + const int d=divisor.val(); + if (d == 0) { + GECODE_ME_CHECK(dividend.eq(home,0)); + return ES_OK; + } + if ((d == 1) || (d == -1)) + return ES_OK; + GECODE_ES_CHECK(divides_multiple_bounds(home,dividend,d)); + } + + // A nonzero dividend excludes a bounds-visible zero divisor. + if ((dividend.min() > 0) || (dividend.max() < 0)) { + if ((divisor.min() == 0) && (divisor.max() > 0)) + GECODE_ME_CHECK(divisor.gq(home,1)); + else if ((divisor.max() == 0) && (divisor.min() < 0)) + GECODE_ME_CHECK(divisor.lq(home,-1)); + } + + // A divisor of an assigned nonzero value has no greater magnitude. + if (dividend.assigned() && (dividend.val() != 0)) { + const int a=(dividend.val() < 0) ? -dividend.val() : dividend.val(); + GECODE_ME_CHECK(divisor.gq(home,-a)); + GECODE_ME_CHECK(divisor.lq(home,a)); + if ((divisor.min() == 0) && (divisor.max() > 0)) + GECODE_ME_CHECK(divisor.gq(home,1)); + else if ((divisor.max() == 0) && (divisor.min() < 0)) + GECODE_ME_CHECK(divisor.lq(home,-1)); + } + + if (divisor.assigned() && dividend.assigned()) + return divides_value(divisor.val(),dividend.val()) ? ES_OK : ES_FAILED; + return ES_FIX; + } + inline RelTest divides_status(const IntView& x0, const IntView& x1) { if (x0 == x1) @@ -50,6 +110,22 @@ namespace Gecode { namespace Int { namespace Arithmetic { return RT_TRUE; if (x0.assigned() && (x0.val() == 0) && !x1.in(0)) return RT_FALSE; + if (x0.assigned() && (x0.val() != 0)) { + const int a=(x0.val() < 0) ? -x0.val() : x0.val(); + const long long int l = + ceil_div_xx(static_cast(x1.min()), + static_cast(a)) * a; + const long long int u = + floor_div_xx(static_cast(x1.max()), + static_cast(a)) * a; + if (l > u) + return RT_FALSE; + } + if (x1.assigned() && (x1.val() != 0)) { + const int a=(x1.val() < 0) ? -x1.val() : x1.val(); + if ((x0.min() > a) || (x0.max() < -a)) + return RT_FALSE; + } if (x0.assigned() && x1.assigned()) return divides_value(x0.val(),x1.val()) ? RT_TRUE : RT_FALSE; return RT_MAYBE; @@ -107,8 +183,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (b.one()) { if (rm == RM_PMI) return home.ES_SUBSUMED(*this); + GECODE_ES_CHECK(divides_bnd(home,x0,x1)); const RelTest rt=divides_status(x0,x1); - if (rt == RT_FALSE) return ES_FAILED; + if (rt == RT_FALSE) + return ES_FAILED; if (rt == RT_TRUE) return home.ES_SUBSUMED(*this); return ES_FIX; diff --git a/gecode/int/arithmetic/gcd.hpp b/gecode/int/arithmetic/gcd.hpp index ad07f227a7..6bc599d37b 100644 --- a/gecode/int/arithmetic/gcd.hpp +++ b/gecode/int/arithmetic/gcd.hpp @@ -52,16 +52,93 @@ namespace Gecode { namespace Int { namespace Arithmetic { (x.max() < 0) ? -x.max() : x.max()); } + forceinline bool + gcd_excludes_zero_bnd(const IntView& x) { + return (x.min() > 0) || (x.max() < 0); + } + + /// Upper bound on a gcd from the operand bounds. + forceinline int + gcd_upper(const IntView& x0, const IntView& x1) { + const int a0=gcd_max_abs(x0); + const int a1=gcd_max_abs(x1); + int u=std::max(a0,a1); + if (gcd_excludes_zero_bnd(x0)) u=std::min(u,a0); + if (gcd_excludes_zero_bnd(x1)) u=std::min(u,a1); + return u; + } + + /// Tighten an operand to the extreme multiples of a positive gcd. + inline ExecStatus + gcd_multiple_bounds(Home home, IntView x, int g) { + assert(g > 0); + const long long int l = + ceil_div_xx(static_cast(x.min()), + static_cast(g)) * g; + const long long int u = + floor_div_xx(static_cast(x.max()), + static_cast(g)) * g; + if (l > u) + return ES_FAILED; + GECODE_ME_CHECK(x.gq(home,static_cast(l))); + GECODE_ME_CHECK(x.lq(home,static_cast(u))); + return ES_OK; + } + + /// Status of abs(x0)=x1 using assignments and interval bounds only. + inline RelTest + gcd_abs_status(const IntView& x0, const IntView& x1) { + if (x1.max() < 0) + return RT_FALSE; + const int l = ((x0.min() <= 0) && (x0.max() >= 0)) ? 0 : + std::min((x0.min() < 0) ? -x0.min() : x0.min(), + (x0.max() < 0) ? -x0.max() : x0.max()); + const int u=gcd_max_abs(x0); + if ((x1.max() < l) || (x1.min() > u)) + return RT_FALSE; + if (x0 == x1) { + if (x0.min() >= 0) + return RT_TRUE; + if (x0.max() < 0) + return RT_FALSE; + } + if (x0.assigned()) { + const int a=(x0.val() < 0) ? -x0.val() : x0.val(); + if (!x1.in(a)) + return RT_FALSE; + return x1.assigned() ? RT_TRUE : RT_MAYBE; + } + if (x1.assigned() && (x1.val() == 0) && !x0.in(0)) + return RT_FALSE; + return RT_MAYBE; + } + inline RelTest gcd_status(const IntView& x0, const IntView& x1, const IntView& x2) { if (x2.max() < 0) return RT_FALSE; - if (x2.min() > std::max(gcd_max_abs(x0),gcd_max_abs(x1))) + if (x2.min() > gcd_upper(x0,x1)) return RT_FALSE; - if (x0 == x1) { - if (x0.assigned() && x2.assigned()) - return gcd_value(x0.val(),x0.val()) == x2.val() ? RT_TRUE : RT_FALSE; - return RT_MAYBE; + if (x0 == x1) + return gcd_abs_status(x0,x2); + if (x0.assigned() && (x0.val() == 0)) + return gcd_abs_status(x1,x2); + if (x1.assigned() && (x1.val() == 0)) + return gcd_abs_status(x0,x2); + if ((x0.assigned() && ((x0.val() == 1) || (x0.val() == -1))) || + (x1.assigned() && ((x1.val() == 1) || (x1.val() == -1)))) { + if (!x2.in(1)) return RT_FALSE; + return x2.assigned() ? RT_TRUE : RT_MAYBE; + } + if (x2.assigned()) { + const int g=x2.val(); + if (g == 0) { + if (!x0.in(0) || !x1.in(0)) return RT_FALSE; + } else if (g > 0) { + if ((x0.assigned() && ((x0.val() % g) != 0)) || + (x1.assigned() && ((x1.val() % g) != 0))) + return RT_FALSE; + } } if (x0.assigned() && x1.assigned()) { const int g=gcd_value(x0.val(),x1.val()); @@ -77,8 +154,35 @@ namespace Gecode { namespace Int { namespace Arithmetic { inline ExecStatus Gcd::post(Home home, IntView x0, IntView x1, IntView x2) { + if (x0 == x1) + return AbsBnd::post(home,x0,x2); + if (x0.assigned() && (x0.val() == 0)) + return AbsBnd::post(home,x1,x2); + if (x1.assigned() && (x1.val() == 0)) + return AbsBnd::post(home,x0,x2); + if ((x0.assigned() && ((x0.val() == 1) || (x0.val() == -1))) || + (x1.assigned() && ((x1.val() == 1) || (x1.val() == -1)))) { + GECODE_ME_CHECK(x2.eq(home,1)); + return ES_OK; + } GECODE_ME_CHECK(x2.gq(home,0)); - GECODE_ME_CHECK(x2.lq(home,std::max(gcd_max_abs(x0),gcd_max_abs(x1)))); + if (gcd_excludes_zero_bnd(x0) || gcd_excludes_zero_bnd(x1)) + GECODE_ME_CHECK(x2.gq(home,1)); + GECODE_ME_CHECK(x2.lq(home,gcd_upper(x0,x1))); + if (x2.assigned()) { + const int g=x2.val(); + if (g == 0) { + GECODE_ME_CHECK(x0.eq(home,0)); + GECODE_ME_CHECK(x1.eq(home,0)); + return ES_OK; + } + GECODE_ES_CHECK(gcd_multiple_bounds(home,x0,g)); + GECODE_ES_CHECK(gcd_multiple_bounds(home,x1,g)); + if (x0.assigned() && (x0.val() == 0)) + return AbsBnd::post(home,x1,x2); + if (x1.assigned() && (x1.val() == 0)) + return AbsBnd::post(home,x0,x2); + } if (x0.assigned() && x1.assigned()) { GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); return ES_OK; @@ -103,8 +207,35 @@ namespace Gecode { namespace Int { namespace Arithmetic { inline ExecStatus Gcd::propagate(Space& home, const ModEventDelta&) { + if (x0 == x1) + GECODE_REWRITE(*this,AbsBnd::post(home(*this),x0,x2)); + if (x0.assigned() && (x0.val() == 0)) + GECODE_REWRITE(*this,AbsBnd::post(home(*this),x1,x2)); + if (x1.assigned() && (x1.val() == 0)) + GECODE_REWRITE(*this,AbsBnd::post(home(*this),x0,x2)); + if ((x0.assigned() && ((x0.val() == 1) || (x0.val() == -1))) || + (x1.assigned() && ((x1.val() == 1) || (x1.val() == -1)))) { + GECODE_ME_CHECK(x2.eq(home,1)); + return home.ES_SUBSUMED(*this); + } GECODE_ME_CHECK(x2.gq(home,0)); - GECODE_ME_CHECK(x2.lq(home,std::max(gcd_max_abs(x0),gcd_max_abs(x1)))); + if (gcd_excludes_zero_bnd(x0) || gcd_excludes_zero_bnd(x1)) + GECODE_ME_CHECK(x2.gq(home,1)); + GECODE_ME_CHECK(x2.lq(home,gcd_upper(x0,x1))); + if (x2.assigned()) { + const int g=x2.val(); + if (g == 0) { + GECODE_ME_CHECK(x0.eq(home,0)); + GECODE_ME_CHECK(x1.eq(home,0)); + return home.ES_SUBSUMED(*this); + } + GECODE_ES_CHECK(gcd_multiple_bounds(home,x0,g)); + GECODE_ES_CHECK(gcd_multiple_bounds(home,x1,g)); + if (x0.assigned() && (x0.val() == 0)) + GECODE_REWRITE(*this,AbsBnd::post(home(*this),x1,x2)); + if (x1.assigned() && (x1.val() == 0)) + GECODE_REWRITE(*this,AbsBnd::post(home(*this),x0,x2)); + } if (x0.assigned() && x1.assigned()) { GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); return home.ES_SUBSUMED(*this); diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 39d113f882..b51bbfe8ed 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -348,36 +348,42 @@ namespace Gecode { namespace Int { namespace Arithmetic { return true; } - /// Find the extreme divisor bounds for a nonzero difference. + /// Tighten divisor bounds for a nonzero difference without enumeration. inline bool product_mod_var_divisor_bounds(int lower, int upper, long long int d, int& least, int& greatest) { const unsigned long long int ad = d < 0 ? static_cast(-(d+1))+1ULL : static_cast(d); - bool found=false; - for (unsigned long long int q=1; q <= ad/q; q++) - if ((ad % q) == 0) { - const unsigned long long int r=ad/q; - if ((q <= static_cast(Limits::max)) && - (q >= static_cast(lower)) && - (q <= static_cast(upper))) { - const int v=static_cast(q); - if (!found) least=greatest=v; - else { least=std::min(least,v); greatest=std::max(greatest,v); } - found=true; - } - if ((r != q) && - (r <= static_cast(Limits::max)) && - (r >= static_cast(lower)) && - (r <= static_cast(upper))) { - const int v=static_cast(r); - if (!found) least=greatest=v; - else { least=std::min(least,v); greatest=std::max(greatest,v); } - found=true; - } - } - return found; + assert(ad > 0); + + least=lower; + greatest=upper; + + // A positive divisor cannot exceed the absolute difference. + if (ad <= static_cast(Limits::max)) + greatest=std::min(greatest,static_cast(ad)); + if (least > greatest) + return false; + + // If floor(ad/m) is fixed throughout the remaining interval, there is + // at most one possible divisor. This is constant-time quotient reasoning; + // it does not inspect the values in the modulus domain. + const unsigned long long int q0 = + ad / static_cast(greatest); + const unsigned long long int q1 = + ad / static_cast(least); + if (q0 == q1) { + if ((q0 == 0) || ((ad % q0) != 0)) + return false; + const unsigned long long int candidate=ad/q0; + if ((candidate < static_cast(least)) || + (candidate > static_cast(greatest)) || + (candidate > static_cast(Limits::max))) + return false; + least=greatest=static_cast(candidate); + } + return true; } /// Determine algebraic status; evaluate the residue only when fully assigned. @@ -416,6 +422,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (!product_mod_var_divisor_bounds (std::max(m.min(),y.val()+1),m.max(),d,least,greatest)) return RT_FALSE; + if ((least == greatest) && !m.in(least)) + return RT_FALSE; } } bool assigned=m.assigned() && y.assigned(); diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index b3676cb17a..d89c22064a 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -1062,7 +1062,7 @@ namespace Test { namespace Int { IntVar m(home,IntSet(mv,10)); IntVar y(home,4,4); product_mod(home,IntVarArgs({a,c}),m,y); - if ((home.status() == SS_FAILED) || (m.min() != 7) || + if ((home.status() == SS_FAILED) || (m.min() != 5) || (m.max() != 56)) return false; } @@ -1118,7 +1118,7 @@ namespace Test { namespace Int { const int hi=Gecode::Int::Limits::max; IntVar x(home,hi,hi), m(home,2,hi), y(home,1,1); product_mod(home,IntVarArgs({x}),m,y); - if ((home.status() == SS_FAILED) || (m.min() != 5) || + if ((home.status() == SS_FAILED) || (m.min() != 2) || (m.max() != hi-1)) return false; } From 06a6851d5433d9f7d9d5041b819d702dbffe0e50 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 8 Sep 2026 18:55:46 +0200 Subject: [PATCH 20/35] Strengthen variable product-mod bounds --- gecode/int/arithmetic/product-mod.hpp | 97 +++++++++++++++++++++++++-- test/int/arithmetic.cpp | 20 ++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index b51bbfe8ed..a786673f67 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -386,6 +386,88 @@ namespace Gecode { namespace Int { namespace Arithmetic { return true; } + /// Propagate a nonnegative representable product with a variable modulus. + inline ExecStatus + product_mod_var_ranges(Home home, ViewArray& x, IntView m, + IntView y, bool& modified) { + modified=false; + for (int i=0; i(p.min) / m.max(); + const long long int kmax = + static_cast(p.max) / m.min(); + if (kmin != kmax) + return ES_OK; + + const long long int k=kmin; + const long long int rmin = + static_cast(p.min)-k*m.max(); + const long long int rmax = + static_cast(p.max)-k*m.min(); + { + ModEvent me=y.gq(home,static_cast(rmin)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me=y.lq(home,static_cast(rmax)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me=m.gq(home,y.min()+1); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + + const long long int tmin=k*m.min()+y.min(); + const long long int tmax=k*m.max()+y.max(); + for (int i=0; i 0) + return ES_FAILED; + continue; + } + const long long int candidate_min=ceil_div_xx(tmin, + static_cast(q.max)); + const long long int candidate_max=(q.min == 0) ? x[i].max() : + floor_div_xx(tmax,static_cast(q.min)); + const long long int l=std::max( + candidate_min,static_cast(x[i].min())); + const long long int u=std::min( + candidate_max,static_cast(x[i].max())); + if (l > u) + return ES_FAILED; + { + ModEvent me=x[i].gq(home,static_cast(l)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + { + ModEvent me=x[i].lq(home,static_cast(u)); + if (me_failed(me)) return ES_FAILED; + modified |= me_modified(me); + } + } + return ES_OK; + } + /// Determine algebraic status; evaluate the residue only when fully assigned. inline RelTest product_mod_var_status(const ViewArray& x, const IntView& m, @@ -443,7 +525,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { IntView modulus, IntView w) : Propagator(home), x(z), m(modulus), y(w) { home.notice(*this,AP_WEAKLY); - x.subscribe(home,*this,PC_INT_VAL); + x.subscribe(home,*this,PC_INT_BND); m.subscribe(home,*this,PC_INT_BND); y.subscribe(home,*this,PC_INT_BND); } @@ -489,12 +571,12 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline PropCost ProductModVar::cost(const Space&, const ModEventDelta&) const { - return PropCost::linear(PropCost::HI,x.size()+2); + return PropCost::quadratic(PropCost::HI,x.size()+2); } forceinline void ProductModVar::reschedule(Space& home) { - x.reschedule(home,*this,PC_INT_VAL); + x.reschedule(home,*this,PC_INT_BND); m.reschedule(home,*this,PC_INT_BND); y.reschedule(home,*this,PC_INT_BND); } @@ -538,6 +620,13 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (m.assigned()) GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); + bool modified; + do { + GECODE_ES_CHECK(product_mod_var_ranges(home,x,m,y,modified)); + } while (modified); + if (m.assigned()) + GECODE_REWRITE(*this,ProductMod::post(home(*this),x,m.val(),y)); + // With an assigned product and result, m must divide product-result. long long int p; if (y.assigned() && product_mod_var_exact(x,p)) { @@ -565,7 +654,7 @@ namespace Gecode { namespace Int { namespace Arithmetic { forceinline size_t ProductModVar::dispose(Space& home) { - x.cancel(home,*this,PC_INT_VAL); + x.cancel(home,*this,PC_INT_BND); m.cancel(home,*this,PC_INT_BND); y.cancel(home,*this,PC_INT_BND); home.ignore(*this,AP_WEAKLY); diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index d89c22064a..1779b45559 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -1003,6 +1003,26 @@ namespace Test { namespace Int { if (home.status() == SS_FAILED) return false; } + { + TestSpace home; + IntVar x(home,3,5), c(home,2,2), m(home,5,6), y(home,2,2); + product_mod(home,IntVarArgs({x,c}),m,y); + if (home.status() == SS_FAILED) + return false; + rel(home,x,IRT_LQ,4); + if ((home.status() == SS_FAILED) || !x.assigned() || + (x.val() != 4)) + return false; + } + { + TestSpace home; + IntVar x(home,10,20), c(home,10,20), m(home,401,509); + IntVar y(home,0,600); + product_mod(home,IntVarArgs({x,c}),m,y); + if ((home.status() == SS_FAILED) || (y.min() != 100) || + (y.max() != 400)) + return false; + } return true; } }; From 788879564ec85b12b1dbf0a1ace267eb504d7821 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 8 Sep 2026 22:10:44 +0200 Subject: [PATCH 21/35] Fix arithmetic review findings and record release readiness --- Makefile.dep | 4 + Makefile.in | 3 +- changelog.in | 19 ++++ docs/gcd-pr-readiness.md | 155 ++++++++++++++++++++++++++++++ gecode/int.hh | 24 ++++- gecode/int/arithmetic/divides.hpp | 4 +- gecode/int/arithmetic/gcd.hpp | 5 +- gecode/int/arithmetic/product.hpp | 8 +- test/int/arithmetic.cpp | 30 ++++++ 9 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 docs/gcd-pr-readiness.md diff --git a/Makefile.dep b/Makefile.dep index 1f26262fd6..667dad0729 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -2144,6 +2144,8 @@ gecode/int/count$(OBJSUFFIX) gecode/int/count$(SBJSUFFIX): \ ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp gecode/int/arithmetic$(OBJSUFFIX) gecode/int/arithmetic$(SBJSUFFIX): \ + ./gecode/int/arithmetic/gcd.hpp ./gecode/int/arithmetic/divides.hpp \ + ./gecode/int/arithmetic/product.hpp ./gecode/int/arithmetic/product-mod.hpp \ ./gecode/int.hh ./gecode/int/arithmetic.hh ./gecode/int/arithmetic/abs.hpp \ ./gecode/int/arithmetic/argmax.hpp ./gecode/int/arithmetic/divmod.hpp ./gecode/int/arithmetic/max.hpp \ ./gecode/int/arithmetic/mult.hpp ./gecode/int/arithmetic/nroot.hpp ./gecode/int/arithmetic/pow-ops.hpp \ @@ -4441,6 +4443,8 @@ gecode/int/branch/chb$(OBJSUFFIX) gecode/int/branch/chb$(SBJSUFFIX): \ ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp gecode/int/arithmetic/mult$(OBJSUFFIX) gecode/int/arithmetic/mult$(SBJSUFFIX): \ + ./gecode/int/arithmetic/gcd.hpp ./gecode/int/arithmetic/divides.hpp \ + ./gecode/int/arithmetic/product.hpp ./gecode/int/arithmetic/product-mod.hpp \ ./gecode/int.hh ./gecode/int/arithmetic.hh ./gecode/int/arithmetic/abs.hpp \ ./gecode/int/arithmetic/argmax.hpp ./gecode/int/arithmetic/divmod.hpp ./gecode/int/arithmetic/max.hpp \ ./gecode/int/arithmetic/mult.hpp ./gecode/int/arithmetic/nroot.hpp ./gecode/int/arithmetic/pow-ops.hpp \ diff --git a/Makefile.in b/Makefile.in index 8141d7ac65..2c67889a1f 100755 --- a/Makefile.in +++ b/Makefile.in @@ -352,7 +352,8 @@ INTHDR0 = \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ arithmetic/abs.hpp arithmetic/max.hpp arithmetic/argmax.hpp \ - arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp arithmetic/product.hpp arithmetic/product-mod.hpp arithmetic/divmod.hpp \ + arithmetic/mult.hpp arithmetic/gcd.hpp arithmetic/divides.hpp \ + arithmetic/product.hpp arithmetic/product-mod.hpp arithmetic/divmod.hpp \ arithmetic/pow-ops.hpp arithmetic/pow.hpp arithmetic/nroot.hpp \ bool/or.hpp bool/eq.hpp bool/lq.hpp bool/eqv.hpp bool/base.hpp \ bool/clause.hpp bool/ite.hpp \ diff --git a/changelog.in b/changelog.in index 4f8129b317..26f0c092b1 100755 --- a/changelog.in +++ b/changelog.in @@ -67,6 +67,25 @@ # optional section in the html page. # +[RELEASE] +Version: 6.5.0 +Date: unreleased +[DESCRIPTION] +This is the development changelog for the next Gecode release. + +[ENTRY] +Module: int +What: new +Rank: major +[DESCRIPTION] +Add ordinary and reified gcd, n-ary product, and n-ary product_mod +constraints, and reified divides. GCD uses nonnegative results and +gcd(0,0)=0; zero divides zero. Modular products accept a fixed positive +modulus or a variable modulus and use nonnegative Euclidean residues, +including for negative products. All reification modes are supported. +Propagation uses bounds and algebraic reasoning; the propagation-level +argument does not select different consistency strengths. + [RELEASE] Version: 6.4.0 Date: 2026-07-15 diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md new file mode 100644 index 0000000000..5d4225afcf --- /dev/null +++ b/docs/gcd-pr-readiness.md @@ -0,0 +1,155 @@ +# Integer arithmetic PR readiness for Gecode 6.5.0 + +Reviewed on 2026-09-08: `feature/gcd` at `06a6851d54`, plus the local +review fixes described below. The feature delta is measured from merge base +`e1ec3da365` with local `main` (`6b7de57b04`, also the locally cached +`origin/main`). Remote refs and CI results were not refreshed. + +## Recommendation + +Close, but I would not yet give an unconditional release-readiness sign-off. +The implementation is a credible addition to 6.5.0: it has a small public API, +explicit mathematical semantics, dedicated propagators, and substantial +solution coverage. This review found no incorrect accepted solutions or lost +solutions, but did reproduce two incorrect fixpoint claims, now fixed. + +Before merging, close the lifecycle-coverage gap below and obtain validation +on the intended integration revision. Add a few independent arithmetic +boundary checks before release. Performance measurements and stronger +propagation are improvements, not reasons by themselves to reject the feature. +No general rewrite or new test framework is warranted. + +## Fixed during this review + +- **GCD and positive reified divisibility claimed a premature fixpoint.** With + `x={1,3,5,6}`, an initially variable divisor/GCD in `1..2`, and then assignment + to 2, both left `x={3,5,6}` after `status()`. For GCD the other operand was 10. + Reposting reduced the domain to `{6}`. Bounds updates had jumped over holes + to another non-multiple, while `propagate()` returned `ES_FIX`. Both now + return `ES_NOFIX` when their own filtering changed a domain. The new + `NumberTheorySparseBounds` regression checks both delayed-assignment cases. +- **Result-aliased products allocated scratch arrays in the space on every + propagation call.** When zero remained possible, the temporary `ViewArray` + was used only to compute an omitted-factor interval and was not retained. + The code now uses the existing omit-index helper, allocating a persistent + array only for an actual rewrite. Existing alias tests cover this path. +- **Autoconf incremental dependencies omitted the four new headers.** Added + them to both affected rules in `Makefile.dep`; checked their inclusion with + `misc/makedepend.py` from the configured build directory. Header installation + was already covered by `Makefile.in` and CMake's directory installation. +- **Public documentation did not explain ignored propagation levels or fixed + modulus exceptions.** The overloads now state the actual propagation + contract, including conservative reified reasoning, ignored `ipl`, and + `Int::OutOfLimits` for invalid fixed moduli even with inactive implications. + The difference from the signed remainder returned by `mod()` is explicit. +- **Release notes omitted the feature.** Added a 6.5.0 entry using the same + unreleased section heading already present on `main`. + +## Remaining work before sign-off + +### 1. Restore targeted lifecycle checks — before merge + +The new generic tests in [test/int/arithmetic.cpp](../test/int/arithmetic.cpp) +all set `testfix=false` and `contest=CTL_NONE`. Disabling consistency checks is +appropriate when bounds/domain consistency is not promised. Disabling every +fixpoint comparison is a separate decision: it helped hide the two defects +found here. + +Several propagators deliberately use `AP_WEAKLY`, so blindly enabling every +reposting comparison would also be wrong. Separate legitimate strengthening +after an interior domain change from failure to finish the propagator's own +bounds updates. Re-enable applicable existing checks and document the specific +reason for each remaining exception. Add only focused regressions for delayed +Boolean activation, sparse endpoints, and clone/reschedule behavior through +rewrites where the existing tests do not establish the invariant. + +Acceptance: own-update fixpoints are tested, weakly monotonic exceptions have +an explicit rationale, and the selected tests run with runtime auditing. +The existing workflow's Debug audit smoke configures and builds an audit +binary but does not execute that binary; extend that validation as part of +this task. See [.github/workflows/build.yml](../.github/workflows/build.yml). + +### 2. Add independent limit-value checks — before release + +Coverage is good for small signed domains, aliases, empty arrays, and all +reification modes. There are also some large-product tests. However, the +product and modular-product test oracles closely follow the implementation's +checked multiplication and modular accumulation. That leaves common mistakes +less likely to be detected independently. + +Use a small set of mathematically known answers, not another general oracle: + +- GCD and divisibility at both `Int::Limits` endpoints, including zero and + negative operands. +- With `M=Int::Limits::max`, `(M-1)^2 mod M = 1`, including a negated factor + and an assigned variable-modulus version. +- A product too large for signed 64-bit arithmetic whose modular residue is + known, followed by assigning the modulus to exercise the fixed-modulus + rewrite. Check the result, not only that propagation has not failed. +- The corresponding false/equivalent and inactive implication cases, so + arithmetic overflow is not confused with a false modular proposition. + +Acceptance: expected values are justified independently, and the cases pass +under an undefined-behavior sanitizer on a supported compiler. + +### 3. Validate the integration revision — before merge/release + +The checks below are local macOS CMake checks, not the release build matrix. +Run the existing supported-platform CI on the revision to be merged, including +Autoconf and installed-header/library consumption. This review did not run +Linux, Windows, 32-bit, sanitizers, an installed consumer, or runtime auditing. +These are unverified areas, not observed platform defects. + +Local `main` has five commits absent from the branch. A read-only merge-tree +check of the committed tips found no conflict markers; it does not validate +the uncommitted fixes or a future remote tip. Integrate the review changes +with current `main`, preserving a single 6.5.0 changelog section, then use the +resulting CI evidence. Canonical version metadata still says 6.4.0 on this +branch; updating release version/SOVERSION is a release-wide decision, not a +reason to change the feature API in this review. + +## Useful improvements that need not block the PR + +- **Measure n-ary scaling before optimizing it.** + [product.hpp](../gecode/int/arithmetic/product.hpp) scans for repeated views + in `product_interval`, then groups them again during inverse propagation. + Both modular propagators recompute omitted-factor intervals in loops. This + gives quadratic work per filtering pass; multiple passes can add more. + Compare a few arities with distinct factors, repeated factors, and result + aliases, recording propagation time and search nodes. For exact products, + use a multiplication decomposition only where intermediate values fit. + Prefix/suffix reuse or grouping once may help, but add no persistent cache + without a measured need. +- **Recognize cheap reified identities earlier.** `product_status` cannot + establish `product([0,x])=0` until all factors are assigned, and a variable + modulus misses some analogous zero-factor entailments. These are safe but + weak cases. Algebraic zero/unit/identity checks could determine the Boolean + or subsume earlier. Preserve the full positivity proposition for a variable + modulus and the inactive implication semantics. +- **Document a short modeling example.** Show Euclidean residues for negative + factors and how to assert divisibility using a true reification variable. + A non-reified `divides` overload would improve symmetry but is not required + for correctness. MiniModel expressions and FlatZinc/MiniZinc exposure were + explicitly excluded by the feature brief and are not missing PR deliverables. + +## Verification performed + +- Release CMake rebuild of `gecode-test`, with no compiler warnings reported. +- All 588 `Int::Arithmetic::` tests passed after the fixes, one iteration, + seed 650, four threads. The original branch also passed this selection. +- Debug CMake rebuild and all 94 selected GCD/divides/product/sparse-bounds + tests passed, three iterations, seed 650, four threads. Runtime audit and + sanitizers were disabled in this existing configuration. +- The delayed sparse-domain probe reproduced the defect against the old + Debug library and reached `{6}` immediately against the fixed Release library. +- Text changelog generation succeeded; `git diff --check` passed. + +Commands for repeating the test selections from the feature worktree: + +```sh +.build-gcd-release/bin/gecode-test -test '^Int::Arithmetic::' -iter 1 -seed 650 -threads 4 +.build-gcd/bin/gecode-test -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheorySparseBounds' -iter 3 -seed 650 -threads 4 +``` + +The Gecode propagator guidance informed the lifecycle and allocation review; +the findings above are based on this branch's code and reproduced behavior. diff --git a/gecode/int.hh b/gecode/int.hh index 59a4fb23ae..0c4d31282d 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -3035,7 +3035,8 @@ namespace Gecode { * * The greatest common divisor is nonnegative, with * \f$\gcd(0,0)=0\f$. Negative operands are interpreted by absolute - * value. Supports sound bounds propagation. + * value. Uses sound bounds and algebraic propagation; bounds consistency + * is not guaranteed. The propagation level \a ipl is currently ignored. */ GECODE_INT_EXPORT void gcd(Home home, IntVar x0, IntVar x1, IntVar x2, @@ -3045,7 +3046,9 @@ namespace Gecode { * \f$(\gcd(x_0,x_1)=x_2)\leftrightarrow r\f$ * * Supports all reification modes. The greatest common divisor is - * nonnegative, with \f$\gcd(0,0)=0\f$. + * nonnegative, with \f$\gcd(0,0)=0\f$. Uses conservative algebraic + * entailment and disentailment tests. The propagation level \a ipl is + * currently ignored. */ GECODE_INT_EXPORT void gcd(Home home, IntVar x0, IntVar x1, IntVar x2, Reify r, @@ -3056,6 +3059,8 @@ namespace Gecode { * Divisibility means that an integer \f$k\f$ exists such that * \f$dividend=divisor\cdot k\f$. Consequently, zero divides zero, but * zero does not divide a nonzero integer. Supports all reification modes. + * Uses bounds and conservative algebraic propagation. The propagation + * level \a ipl is currently ignored. */ GECODE_INT_EXPORT void divides(Home home, IntVar divisor, IntVar dividend, Reify r, @@ -3064,6 +3069,8 @@ namespace Gecode { /** \brief Constrain \a y to the exact product of the variables in \a x * * The product of an empty array is one. + * Uses bounds and algebraic propagation, without guaranteeing bounds + * consistency. The propagation level \a ipl is currently ignored. * \ingroup TaskModelInt */ GECODE_INT_EXPORT void @@ -3073,6 +3080,8 @@ namespace Gecode { /** \brief Reify whether \a y is the exact product of the variables in \a x * * The product of an empty array is one. + * Supports all reification modes using conservative algebraic tests. + * The propagation level \a ipl is currently ignored. * \ingroup TaskModelInt */ GECODE_INT_EXPORT void @@ -3084,6 +3093,9 @@ namespace Gecode { * The modulus \a m must be positive. The result uses the canonical * Euclidean residue in the range zero through \a m minus one. The product * of an empty array is one. + * Unlike mod(), a negative product still has a nonnegative residue. + * Uses bounds and algebraic propagation; \a ipl is currently ignored. + * Throws Int::OutOfLimits if \a m is nonpositive or exceeds Int::Limits::max. * \ingroup TaskModelInt */ GECODE_INT_EXPORT void @@ -3095,6 +3107,10 @@ namespace Gecode { * The modulus \a m must be positive. The result uses the canonical * Euclidean residue in the range zero through \a m minus one. The product * of an empty array is one. + * Supports all reification modes using conservative algebraic tests; + * \a ipl is currently ignored. + * Throws Int::OutOfLimits if \a m is nonpositive or exceeds Int::Limits::max, + * even for an inactive implication. * \ingroup TaskModelInt */ GECODE_INT_EXPORT void @@ -3106,6 +3122,7 @@ namespace Gecode { * The variable modulus \a m is constrained to be positive and \a y uses * the canonical Euclidean residue, so that \f$0\leq y0\f$, the canonical range * \f$0\leq y::post(home(*this),x0,x2)); if (x0.assigned() && (x0.val() == 0)) @@ -240,7 +241,9 @@ namespace Gecode { namespace Int { namespace Arithmetic { GECODE_ME_CHECK(x2.eq(home,gcd_value(x0.val(),x1.val()))); return home.ES_SUBSUMED(*this); } - return ES_FIX; + // A bounds update can jump over a hole to another non-multiple. + return ((s0 != x0.size()) || (s1 != x1.size()) || (s2 != x2.size())) + ? ES_NOFIX : ES_FIX; } template diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index 6fdcde3333..f9117b9f60 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -300,15 +300,15 @@ namespace Gecode { namespace Int { namespace Arithmetic { if (alias >= 0) { if (y.assigned() && (y.val() == 0)) return home.ES_SUBSUMED(*this); - ViewArray z(home,x.size()-1); - for (int i=0, j=0; i z(home,x.size()-1); + for (int i=0, j=0; i q.max)) { GECODE_ME_CHECK(y.eq(home,0)); return home.ES_SUBSUMED(*this); diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 1779b45559..179058e6cb 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -203,6 +203,35 @@ namespace Test { namespace Int { } }; + /// Sparse endpoints must reach a fixpoint after a divisor is assigned. + class NumberTheorySparseBounds : public ::Test::Base { + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + NumberTheorySparseBounds(void) + : ::Test::Base("Int::Arithmetic::NumberTheorySparseBounds") {} + virtual bool run(void) { + using namespace Gecode; + for (int division=0; division<2; division++) { + TestSpace home; + IntVar x(home,IntSet({1,3,5,6})), y(home,10,10), g(home,1,2); + BoolVar b(home,1,1); + if (division) + divides(home,g,x,Reify(b)); + else + gcd(home,x,y,g); + if (home.status() == SS_FAILED) + return false; + rel(home,g,IRT_EQ,2); + if ((home.status() == SS_FAILED) || !x.assigned() || (x.val()!=6)) + return false; + } + return true; + } + }; + /// Evaluate an exact product for testing without overflowing. bool product_value(const Assignment& x, int n, int& product) { for (int i=0; i Date: Tue, 8 Sep 2026 22:15:00 +0200 Subject: [PATCH 22/35] Exercise arithmetic activation cloning and audit checks --- .github/workflows/build.yml | 6 +++ docs/gcd-pr-readiness.md | 11 +++++ test/int/arithmetic.cpp | 82 +++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7630dcdeff..0e8de43b1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -251,6 +251,12 @@ jobs: shell: bash run: cmake --build "$GITHUB_WORKSPACE/build-ninja-debug" + - name: Run Debug audit arithmetic tests + shell: bash + run: > + "$GITHUB_WORKSPACE/build-ninja-debug/bin/gecode-test" + -test '^Int::Arithmetic::' -iter 1 -seed 650 -threads 2 + - name: Install and smoke test FlatZinc tools shell: bash run: | diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index 5d4225afcf..99c19d9e55 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -7,6 +7,17 @@ review fixes described below. The feature delta is measured from merge base ## Recommendation +Implementation follow-up: the initial fixes are committed as `788879564e`. +Targeted lifecycle tests now cover delayed Boolean activation, disabled actor +rescheduling, cloning before and after activation, and the variable-to-fixed +modulus rewrite. The GCD-all-aliased and divides-self tests now retain the +generic fixpoint comparisons. Other exceptions have family-specific comments +explaining the interior membership observations missed by bounds events. +For example, enabling the empty-product comparison reproduces that legitimate +exception (seed 1576866552, fixprob 1). All 95 focused Release tests pass with +three iterations, seed 650, and fixprob 1. The workflow now executes its Debug +audit arithmetic binary; local audit/UBSan validation is in progress. + Close, but I would not yet give an unconditional release-readiness sign-off. The implementation is a credible addition to 6.5.0: it has a small public API, explicit mathematical semantics, dedicated propagators, and substantial diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 179058e6cb..09cf870c57 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -35,6 +35,7 @@ #include #include +#include #include @@ -60,6 +61,10 @@ namespace Test { namespace Int { } /// %Test for the ternary greatest-common-divisor constraint + // Reposting comparisons below are disabled where bounds subscriptions + // deliberately miss interior removals used by algebraic status tests. + // NumberTheoryLifecycle checks activation, cloning, and rescheduling + // separately; NumberTheorySparseBounds checks own-update fixpoints. class GcdXYZ : public Test { public: /// Create and register test @@ -138,7 +143,7 @@ namespace Test { namespace Int { GcdXXX(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Gcd::XXX::"+str(ipl)+"::"+s,1,d,true,ipl) { - contest=CTL_NONE; testfix=false; + contest=CTL_NONE; } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { @@ -156,6 +161,8 @@ namespace Test { namespace Int { }; /// %Test for the reified divisibility constraint + // With divisor zero, removing interior zero from the dividend can + // strengthen a reposted status test without a bounds wakeup. class DividesXY : public Test { public: /// Create and register test @@ -186,7 +193,7 @@ namespace Test { namespace Int { DividesXX(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Divides::XX::"+str(ipl)+"::"+s, - 1,d,true,ipl) { contest=CTL_NONE; testfix=false; } + 1,d,true,ipl) { contest=CTL_NONE; } /// Every integer divides itself, including zero virtual bool solution(const Assignment&) const { return true; @@ -232,6 +239,64 @@ namespace Test { namespace Int { } }; + /// Activation and rescheduling survive cloning and arithmetic rewrites. + class NumberTheoryLifecycle : public ::Test::Base { + class TestSpace : public Gecode::Space { + public: + Gecode::IntVar x, c, y, m; + Gecode::BoolVar b; + TestSpace(int kind) + : x(*this,Gecode::IntSet({1,3,5,6})), + c(*this,kind == 0 ? 10 : 2,kind == 0 ? 10 : 2), + y(*this,kind == 0 ? 2 : (kind == 4 ? 1 : 12), + kind == 0 ? 2 : (kind == 4 ? 1 : 12)), + m(*this,kind == 4 ? 5 : 13,kind == 4 ? 7 : 17), b(*this,0,1) { + using namespace Gecode; + switch (kind) { + case 0: gcd(*this,x,c,y,Reify(b)); break; + case 1: divides(*this,c,x,Reify(b)); break; + case 2: product(*this,IntVarArgs({x,c}),y,Reify(b)); break; + case 3: product_mod(*this,IntVarArgs({x,c}),13,y,Reify(b)); break; + case 4: product_mod(*this,IntVarArgs({x,c}),m,y,Reify(b)); break; + default: GECODE_NEVER; + } + } + TestSpace(TestSpace& s) : Gecode::Space(s) { + x.update(*this,s.x); c.update(*this,s.c); y.update(*this,s.y); + m.update(*this,s.m); b.update(*this,s.b); + } + virtual Gecode::Space* copy(void) { return new TestSpace(*this); } + }; + public: + NumberTheoryLifecycle(void) + : ::Test::Base("Int::Arithmetic::NumberTheoryLifecycle") {} + virtual bool run(void) { + using namespace Gecode; + for (int kind=0; kind<5; kind++) { + TestSpace home(kind); + if (home.status() == SS_FAILED) + return false; + std::unique_ptr clone + (static_cast(home.clone())); + PropagatorGroup::all.disable(*clone); + rel(*clone,clone->b,IRT_EQ,1); + PropagatorGroup::all.enable(*clone); + if ((clone->status() == SS_FAILED) || + ((kind != 4) && (!clone->x.assigned() || (clone->x.val() != 6)))) + return false; + // Clone the activated actor before fixing a variable modulus. + std::unique_ptr child + (static_cast(clone->clone())); + rel(*child,child->m,IRT_EQ,kind == 4 ? 5 : 13); + if ((child->status() == SS_FAILED) || !child->x.assigned() || + (child->x.val() != (kind == 4 ? 3 : 6)) || + home.b.assigned() || (home.x.size() != 4) || clone->m.assigned()) + return false; + } + return true; + } + }; + /// Evaluate an exact product for testing without overflowing. bool product_value(const Assignment& x, int n, int& product) { for (int i=0; i Date: Tue, 8 Sep 2026 22:17:44 +0200 Subject: [PATCH 23/35] Check number theory limits with independent identities --- docs/gcd-pr-readiness.md | 6 ++++ test/int/arithmetic.cpp | 74 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index 99c19d9e55..6ca8cebc7a 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -18,6 +18,12 @@ exception (seed 1576866552, fixprob 1). All 95 focused Release tests pass with three iterations, seed 650, and fixprob 1. The workflow now executes its Debug audit arithmetic binary; local audit/UBSan validation is in progress. +Independent limit checks now cover GCD/divides at signed endpoints, positive +and negative modular squares/cubes, delayed modulus assignment, and all +reification truth/control combinations. These cases pass with auditing and +UBSan (`halt_on_error=1`). A broader UBSan run exposed pre-existing signed +overflow in the `DivMod` test oracle; it is being corrected separately. + Close, but I would not yet give an unconditional release-readiness sign-off. The implementation is a credible addition to 6.5.0: it has a small public API, explicit mathematical semantics, dedicated propagators, and substantial diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 09cf870c57..bb746bdb17 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -297,6 +297,79 @@ namespace Test { namespace Int { } }; + /// Known identities at the integer limits, independent of the oracles. + class NumberTheoryLimits : public ::Test::Base { + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + NumberTheoryLimits(void) + : ::Test::Base("Int::Arithmetic::NumberTheoryLimits") {} + virtual bool run(void) { + using namespace Gecode; + const int hi=Gecode::Int::Limits::max; + const int lo=Gecode::Int::Limits::min; + const int cases[][4] = { + {lo,hi,hi,1}, {hi,lo,hi,1}, {lo,0,hi,1}, + {0,lo,hi,0}, {0,0,0,1}, {lo,1,1,0} + }; + for (const auto& c : cases) { + TestSpace home; + IntVar x(home,c[0],c[0]), y(home,c[1],c[1]), g(home,0,hi); + BoolVar b(home,0,1), correct(home,0,1), wrong(home,0,1); + gcd(home,x,y,g); + divides(home,x,y,Reify(b)); + IntVar expected(home,c[2],c[2]); + IntVar other(home,c[2] == 0 ? 1 : 0,c[2] == 0 ? 1 : 0); + gcd(home,x,y,expected,Reify(correct)); + gcd(home,x,y,other,Reify(wrong)); + if ((home.status() == SS_FAILED) || !g.assigned() || + (g.val() != c[2]) || !b.assigned() || (b.val() != c[3]) || + !correct.assigned() || (correct.val() != 1) || + !wrong.assigned() || (wrong.val() != 0)) + return false; + } + // hi-1 is -1 modulo hi. Its square is 1 and its cube is -1; + // negating one factor reverses these residues. The cube exceeds + // signed 64-bit range, but its residue remains exactly known. + for (int n=2; n<=3; n++) + for (int negative=0; negative<=1; negative++) + for (int variable=0; variable<=1; variable++) { + const int expected=((n+negative)%2 == 0) ? 1 : hi-1; + { + TestSpace home; + IntVarArgs x(home,n,hi-1,hi-1); + if (negative) x[0]=IntVar(home,1-hi,1-hi); + IntVar m(home,hi-1,hi), y(home,0,hi-1); + if (variable) product_mod(home,x,m,y); + else product_mod(home,x,hi,y); + if (home.status() == SS_FAILED) return false; + // Force variable-modulus rewriting after initial propagation. + rel(home,m,IRT_EQ,hi); + if ((home.status() == SS_FAILED) || !y.assigned() || + (y.val() != expected)) return false; + } + for (ReifyMode rm : {RM_EQV,RM_IMP,RM_PMI}) + for (int truth=0; truth<=1; truth++) + for (int control=0; control<=1; control++) { + TestSpace home; + IntVarArgs x(home,n,hi-1,hi-1); + if (negative) x[0]=IntVar(home,1-hi,1-hi); + IntVar m(home,hi,hi); + IntVar y(home,truth ? expected : 0,truth ? expected : 0); + BoolVar b(home,control,control); + if (variable) product_mod(home,x,m,y,Reify(b,rm)); + else product_mod(home,x,hi,y,Reify(b,rm)); + const bool holds=(rm == RM_EQV) ? (control == truth) : + ((rm == RM_IMP) ? (!control || truth) : (!truth || control)); + if ((home.status() != SS_FAILED) != holds) return false; + } + } + return true; + } + }; + /// Evaluate an exact product for testing without overflowing. bool product_value(const Assignment& x, int n, int& product) { for (int i=0; i Date: Tue, 8 Sep 2026 22:22:14 +0200 Subject: [PATCH 24/35] Recognize reified product identities and harden arithmetic tests --- docs/integer-number-theory.md | 42 +++++++++++++++++++++++++++ gecode/int/arithmetic/product-mod.hpp | 7 +++-- gecode/int/arithmetic/product.hpp | 7 +++++ test/int/arithmetic.cpp | 40 +++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 docs/integer-number-theory.md diff --git a/docs/integer-number-theory.md b/docs/integer-number-theory.md new file mode 100644 index 0000000000..323a35f1c1 --- /dev/null +++ b/docs/integer-number-theory.md @@ -0,0 +1,42 @@ +# Integer number theory constraints + +These APIs require ``. They use bounds and algebraic reasoning; +the `IntPropLevel` argument currently does not select different strengths. +They do not promise bounds or domain consistency. + +Inside a `Space` constructor, this model has `g=6`, `p=-216`, and `r=1`: + +```cpp +IntVar x(*this,-12,-12), y(*this,18,18); +IntVar g(*this,0,18), p(*this,-300,300), r(*this,0,6); +gcd(*this,x,y,g); +product(*this,IntVarArgs({x,y}),p); +product_mod(*this,IntVarArgs({x,y}),7,r); +``` + +Call `status()` on the space to run propagation. `product_mod` uses the +Euclidean residue: `-216 = -31*7 + 1`. In contrast, Gecode's `mod` uses a +dividend-signed remainder and would return `-6` for `-216 mod 7`. + +To require divisibility, pass a true reification variable: + +```cpp +BoolVar yes(*this,1,1); +IntVar divisor(*this,6,6), dividend(*this,-30,30); +divides(*this,divisor,dividend,Reify(yes)); +``` + +This relation means that some integer multiplier exists. Zero divides zero, +but does not divide a nonzero value. GCD is always nonnegative, with +`gcd(0,0)=0`. The exact product of an empty array is one; its modular product +is `1 mod m`, which is zero when `m=1`. + +Every reified overload supports equivalence (`RM_EQV`), `b` implying the +relation (`RM_IMP`), and the relation implying `b` (`RM_PMI`). For example, +`product_mod(*this,factors,m,result,Reify(enabled,RM_IMP))` requires the +modular relation only when `enabled=1`. + +A fixed integer modulus must lie in `1..Int::Limits::max`; an invalid constant +throws `Int::OutOfLimits` even when an implication is inactive. With an +`IntVar` modulus, positivity is part of the reified proposition. Consequently, +an inactive implication does not constrain that modulus or the result. diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index a786673f67..45f9303977 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -475,8 +475,11 @@ namespace Gecode { namespace Int { namespace Arithmetic { if ((m == y) || (m.max() <= 0) || (y.max() < 0) || (y.min() >= m.max())) return RT_FALSE; - const bool mf=product_mod_var_mod_factor(x,m); - if (mf) { + bool zero=product_mod_var_mod_factor(x,m) || + (m.assigned() && (m.val() == 1)); + for (int i=0; !zero && (i 0) && y.assigned()) return RT_TRUE; return RT_MAYBE; diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index f9117b9f60..70636341f8 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -199,6 +199,13 @@ namespace Gecode { namespace Int { namespace Arithmetic { /// Determine the relation when bounds or assignments prove it. inline RelTest product_status(const ViewArray& x, const IntView& y) { + if ((x.size() == 1) && (x[0] == y)) + return RT_TRUE; + for (int i=0; i y.max())) return RT_FALSE; diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index bb746bdb17..8d54c3b320 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -297,6 +297,41 @@ namespace Test { namespace Int { } }; + /// Reified identities do not require the remaining factors to be fixed. + class NumberTheoryIdentities : public ::Test::Base { + class TestSpace : public Gecode::Space { + public: + virtual Gecode::Space* copy(void) { return nullptr; } + }; + public: + NumberTheoryIdentities(void) + : ::Test::Base("Int::Arithmetic::NumberTheoryIdentities") {} + virtual bool run(void) { + using namespace Gecode; + for (ReifyMode rm : {RM_EQV,RM_IMP,RM_PMI}) + for (int kind=0; kind<4; kind++) { + TestSpace home; + IntVar x(home,-7,7), zero(home,0,0), m(home,0,7), one(home,1,1); + BoolVar b(home,0,1); + if (kind == 0) product(home,IntVarArgs({zero,x}),zero,Reify(b,rm)); + if (kind == 1) product(home,IntVarArgs({x}),x,Reify(b,rm)); + if (kind == 2) product_mod(home,IntVarArgs({zero,x}),m,zero,Reify(b,rm)); + if (kind == 3) product_mod(home,IntVarArgs({x}),one,zero,Reify(b,rm)); + if (home.status() == SS_FAILED) return false; + if (kind == 2) { + // Zero residue alone does not establish a positive modulus. + if (b.assigned() || (m.min() != 0)) return false; + rel(home,m,IRT_GQ,1); + if (home.status() == SS_FAILED) return false; + } + if ((rm == RM_IMP) ? b.assigned() : + (!b.assigned() || (b.val() != 1))) return false; + if ((x.min() != -7) || (x.max() != 7)) return false; + } + return true; + } + }; + /// Known identities at the integer limits, independent of the oracles. class NumberTheoryLimits : public ::Test::Base { class TestSpace : public Gecode::Space { @@ -1823,7 +1858,7 @@ namespace Test { namespace Int { : Test("Arithmetic::DivMod::"+s,4,d) {} /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { - return x[0] == x[1]*x[2]+x[3] && + return x[0] == static_cast(x[1])*x[2]+x[3] && abs(x[3]) < abs(x[1]) && (x[3] == 0 || sgn(x[3]) == sgn(x[0])); } @@ -1871,7 +1906,7 @@ namespace Test { namespace Int { divsign * static_cast(floor(static_cast(std::abs(x[0]))/ static_cast(std::abs(x[1])))); - return x[0] == x[1]*divresult+x[2]; + return x[0] == static_cast(x[1])*divresult+x[2]; } /// Post constraint on \a x virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { @@ -2707,6 +2742,7 @@ namespace Test { namespace Int { (void) new NumberTheorySparseBounds; (void) new NumberTheoryLifecycle; (void) new NumberTheoryLimits; + (void) new NumberTheoryIdentities; (void) new ProductModInvalidModulus; (void) new ProductModAlgebraic; (void) new ProductModVarBounds; From 8c132a65dbe85043ac3ab33c892a48841c86e4fd Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 8 Sep 2026 22:28:35 +0200 Subject: [PATCH 25/35] Record readiness validation and product scaling measurements --- docs/gcd-pr-readiness.md | 303 +++++++++++++++-------------------- misc/bench-number-theory.cpp | 50 ++++++ misc/bench-number-theory.py | 22 +++ 3 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 misc/bench-number-theory.cpp create mode 100644 misc/bench-number-theory.py diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index 6ca8cebc7a..ac4bb65ca7 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -1,172 +1,131 @@ -# Integer arithmetic PR readiness for Gecode 6.5.0 - -Reviewed on 2026-09-08: `feature/gcd` at `06a6851d54`, plus the local -review fixes described below. The feature delta is measured from merge base -`e1ec3da365` with local `main` (`6b7de57b04`, also the locally cached -`origin/main`). Remote refs and CI results were not refreshed. - -## Recommendation - -Implementation follow-up: the initial fixes are committed as `788879564e`. -Targeted lifecycle tests now cover delayed Boolean activation, disabled actor -rescheduling, cloning before and after activation, and the variable-to-fixed -modulus rewrite. The GCD-all-aliased and divides-self tests now retain the -generic fixpoint comparisons. Other exceptions have family-specific comments -explaining the interior membership observations missed by bounds events. -For example, enabling the empty-product comparison reproduces that legitimate -exception (seed 1576866552, fixprob 1). All 95 focused Release tests pass with -three iterations, seed 650, and fixprob 1. The workflow now executes its Debug -audit arithmetic binary; local audit/UBSan validation is in progress. - -Independent limit checks now cover GCD/divides at signed endpoints, positive -and negative modular squares/cubes, delayed modulus assignment, and all -reification truth/control combinations. These cases pass with auditing and -UBSan (`halt_on_error=1`). A broader UBSan run exposed pre-existing signed -overflow in the `DivMod` test oracle; it is being corrected separately. - -Close, but I would not yet give an unconditional release-readiness sign-off. -The implementation is a credible addition to 6.5.0: it has a small public API, -explicit mathematical semantics, dedicated propagators, and substantial -solution coverage. This review found no incorrect accepted solutions or lost -solutions, but did reproduce two incorrect fixpoint claims, now fixed. - -Before merging, close the lifecycle-coverage gap below and obtain validation -on the intended integration revision. Add a few independent arithmetic -boundary checks before release. Performance measurements and stronger -propagation are improvements, not reasons by themselves to reject the feature. -No general rewrite or new test framework is warranted. - -## Fixed during this review - -- **GCD and positive reified divisibility claimed a premature fixpoint.** With - `x={1,3,5,6}`, an initially variable divisor/GCD in `1..2`, and then assignment - to 2, both left `x={3,5,6}` after `status()`. For GCD the other operand was 10. - Reposting reduced the domain to `{6}`. Bounds updates had jumped over holes - to another non-multiple, while `propagate()` returned `ES_FIX`. Both now - return `ES_NOFIX` when their own filtering changed a domain. The new - `NumberTheorySparseBounds` regression checks both delayed-assignment cases. -- **Result-aliased products allocated scratch arrays in the space on every - propagation call.** When zero remained possible, the temporary `ViewArray` - was used only to compute an omitted-factor interval and was not retained. - The code now uses the existing omit-index helper, allocating a persistent - array only for an actual rewrite. Existing alias tests cover this path. -- **Autoconf incremental dependencies omitted the four new headers.** Added - them to both affected rules in `Makefile.dep`; checked their inclusion with - `misc/makedepend.py` from the configured build directory. Header installation - was already covered by `Makefile.in` and CMake's directory installation. -- **Public documentation did not explain ignored propagation levels or fixed - modulus exceptions.** The overloads now state the actual propagation - contract, including conservative reified reasoning, ignored `ipl`, and - `Int::OutOfLimits` for invalid fixed moduli even with inactive implications. - The difference from the signed remainder returned by `mod()` is explicit. -- **Release notes omitted the feature.** Added a 6.5.0 entry using the same - unreleased section heading already present on `main`. - -## Remaining work before sign-off - -### 1. Restore targeted lifecycle checks — before merge - -The new generic tests in [test/int/arithmetic.cpp](../test/int/arithmetic.cpp) -all set `testfix=false` and `contest=CTL_NONE`. Disabling consistency checks is -appropriate when bounds/domain consistency is not promised. Disabling every -fixpoint comparison is a separate decision: it helped hide the two defects -found here. - -Several propagators deliberately use `AP_WEAKLY`, so blindly enabling every -reposting comparison would also be wrong. Separate legitimate strengthening -after an interior domain change from failure to finish the propagator's own -bounds updates. Re-enable applicable existing checks and document the specific -reason for each remaining exception. Add only focused regressions for delayed -Boolean activation, sparse endpoints, and clone/reschedule behavior through -rewrites where the existing tests do not establish the invariant. - -Acceptance: own-update fixpoints are tested, weakly monotonic exceptions have -an explicit rationale, and the selected tests run with runtime auditing. -The existing workflow's Debug audit smoke configures and builds an audit -binary but does not execute that binary; extend that validation as part of -this task. See [.github/workflows/build.yml](../.github/workflows/build.yml). - -### 2. Add independent limit-value checks — before release - -Coverage is good for small signed domains, aliases, empty arrays, and all -reification modes. There are also some large-product tests. However, the -product and modular-product test oracles closely follow the implementation's -checked multiplication and modular accumulation. That leaves common mistakes -less likely to be detected independently. - -Use a small set of mathematically known answers, not another general oracle: - -- GCD and divisibility at both `Int::Limits` endpoints, including zero and - negative operands. -- With `M=Int::Limits::max`, `(M-1)^2 mod M = 1`, including a negated factor - and an assigned variable-modulus version. -- A product too large for signed 64-bit arithmetic whose modular residue is - known, followed by assigning the modulus to exercise the fixed-modulus - rewrite. Check the result, not only that propagation has not failed. -- The corresponding false/equivalent and inactive implication cases, so - arithmetic overflow is not confused with a false modular proposition. - -Acceptance: expected values are justified independently, and the cases pass -under an undefined-behavior sanitizer on a supported compiler. - -### 3. Validate the integration revision — before merge/release - -The checks below are local macOS CMake checks, not the release build matrix. -Run the existing supported-platform CI on the revision to be merged, including -Autoconf and installed-header/library consumption. This review did not run -Linux, Windows, 32-bit, sanitizers, an installed consumer, or runtime auditing. -These are unverified areas, not observed platform defects. - -Local `main` has five commits absent from the branch. A read-only merge-tree -check of the committed tips found no conflict markers; it does not validate -the uncommitted fixes or a future remote tip. Integrate the review changes -with current `main`, preserving a single 6.5.0 changelog section, then use the -resulting CI evidence. Canonical version metadata still says 6.4.0 on this -branch; updating release version/SOVERSION is a release-wide decision, not a -reason to change the feature API in this review. - -## Useful improvements that need not block the PR - -- **Measure n-ary scaling before optimizing it.** - [product.hpp](../gecode/int/arithmetic/product.hpp) scans for repeated views - in `product_interval`, then groups them again during inverse propagation. - Both modular propagators recompute omitted-factor intervals in loops. This - gives quadratic work per filtering pass; multiple passes can add more. - Compare a few arities with distinct factors, repeated factors, and result - aliases, recording propagation time and search nodes. For exact products, - use a multiplication decomposition only where intermediate values fit. - Prefix/suffix reuse or grouping once may help, but add no persistent cache - without a measured need. -- **Recognize cheap reified identities earlier.** `product_status` cannot - establish `product([0,x])=0` until all factors are assigned, and a variable - modulus misses some analogous zero-factor entailments. These are safe but - weak cases. Algebraic zero/unit/identity checks could determine the Boolean - or subsume earlier. Preserve the full positivity proposition for a variable - modulus and the inactive implication semantics. -- **Document a short modeling example.** Show Euclidean residues for negative - factors and how to assert divisibility using a true reification variable. - A non-reified `divides` overload would improve symmetry but is not required - for correctness. MiniModel expressions and FlatZinc/MiniZinc exposure were - explicitly excluded by the feature brief and are not missing PR deliverables. - -## Verification performed - -- Release CMake rebuild of `gecode-test`, with no compiler warnings reported. -- All 588 `Int::Arithmetic::` tests passed after the fixes, one iteration, - seed 650, four threads. The original branch also passed this selection. -- Debug CMake rebuild and all 94 selected GCD/divides/product/sparse-bounds - tests passed, three iterations, seed 650, four threads. Runtime audit and - sanitizers were disabled in this existing configuration. -- The delayed sparse-domain probe reproduced the defect against the old - Debug library and reached `{6}` immediately against the fixed Release library. -- Text changelog generation succeeded; `git diff --check` passed. - -Commands for repeating the test selections from the feature worktree: - -```sh -.build-gcd-release/bin/gecode-test -test '^Int::Arithmetic::' -iter 1 -seed 650 -threads 4 -.build-gcd/bin/gecode-test -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheorySparseBounds' -iter 3 -seed 650 -threads 4 -``` - -The Gecode propagator guidance informed the lifecycle and allocation review; -the findings above are based on this branch's code and reproduced behavior. +# Integer arithmetic readiness for Gecode 6.5.0 + +## Assessment + +The local implementation and verification work is complete. The remaining +release gate is the supported-platform CI for [draft PR #240](https://github.com/Gecode/gecode/pull/240). +This is not yet a claim that the complete release matrix is green. + +Reviewed initially at `06a6851d54`; fixes and follow-up work are integrated +with `origin/main` at `6b7de57b04` in merge commit `00d8d061ae`. +The changelog contains one unreleased 6.5.0 section. Version/SOVERSION changes +remain a release-wide responsibility. + +## Completed work + +- Fixed premature fixpoint returns in GCD and positive reified divisibility. + A delayed divisor/GCD assignment to 2 previously left `x={3,5,6}` after + propagation, although reposting reduced it to `{6}`. Domain changes now + request another propagation pass. A focused regression covers both cases. +- Removed repeated space allocation for temporary result-alias product arrays. + The existing omit-index interval helper now handles that case; allocation + remains only when a rewrite retains the new array. +- Updated Autoconf dependencies for all four headers, public API propagation + and exception documentation, and the 6.5.0 release note. +- Added activation, disable/enable rescheduling, cloning, and variable-to-fixed + modulus rewrite checks. Restored generic fixpoint comparisons for + `gcd(x,x,x)` and `divides(x,x)`. +- Added independent endpoint and modular square/cube identities, including + products beyond signed 64-bit range, delayed modulus assignment, negative + factors, and all reification truth/control combinations. +- Recognize zero products and singleton result aliases in reified exact + products, and zero-factor/modulus-one identities with variable moduli. + Positivity remains part of the proposition; the tests check that distinction. +- Audited UBSan testing exposed pre-existing overflow in the `DivMod` and + `Mod` test oracles. Their arithmetic now uses signed 64-bit intermediates. +- The existing Debug audit CI job now executes its arithmetic tests. +- Added a [modeling example](integer-number-theory.md) covering signs, + Euclidean residues, divisibility, and implication semantics. + +The Gecode skill informed the lifecycle and memory review. No new propagation +framework, persistent cache, or support enumeration was introduced. + +## Lifecycle exceptions + +`contest=CTL_NONE` remains appropriate: these propagators do not promise +bounds or domain consistency. Most generic reposting comparisons still use +`testfix=false` for a different, documented reason: + +| Family | Interior membership that can strengthen reposting without a bounds event | +| --- | --- | +| Reified GCD | Zero or the GCD computed from assigned operands | +| Reified divides | Zero in the dividend when the divisor is zero | +| Exact product | Zero in factors/result, or an assigned product in the result | +| Fixed modular product | The computed residue, including the empty-product identity | +| Variable modular product | Zero/one in the result or a derived divisor in the modulus | + +These are intentional weak-monotonicity cases, not a license to stop before +finishing a propagator's own updates. Family-specific test comments explain +them. Enabling the empty-product comparison reproduced the distinction with +seed 1576866552 and fixprob 1. `NumberTheorySparseBounds` and +`NumberTheoryLifecycle` check own-update completion and actor transitions +separately; applicable alias identity comparisons are enabled. + +## Local validation + +- All **591 integer arithmetic tests** passed in Release. +- The same **591 tests** passed in Debug with runtime auditing and UBSan, + with `UBSAN_OPTIONS=halt_on_error=1`. +- The earlier focused lifecycle selection passed three iterations with + seed 650 and fixprob 1; the completed full-suite runs used one iteration, + seed 650, and four threads. +- A CMake installation and a separate `find_package(Gecode CONFIG)` + consumer compiled, linked, and executed all nine public overloads. + Including the installed `gecode/int/arithmetic.hh` also checked the new + implementation headers. Results were GCD 6, product -216, residue 1. +- Text changelog generation, repository tidy checks, and + `git diff --check` passed. + +The local host was an Apple M1 Max with Apple Clang 21.0.0. No local Linux, +Windows, or 32-bit execution is implied by these results. Platform coverage +comes from the PR's existing CI matrix, including Autoconf and package checks. + +## Scaling sample + +This small experiment measures model construction, posting, initial +propagation, and destruction together; it is not an isolated propagation +microbenchmark. Factors have domain `0..1`, so the multiplication-chain +baseline has representable intermediates. Twenty warmup constructions precede +500 measured constructions per sample; results are medians of three samples, +in microseconds. Search is measured separately and exhaustively only at +arities 4 and 12, with the same branching order and solution count checks. + +| Arity | Factor pattern | Product (us) | Chain (us) | +| --- | --- | ---: | ---: | +| 4 | distinct | 0.446 | 0.371 | +| 4 | repeated | 0.379 | 0.435 | +| 4 | result alias | 0.475 | 0.362 | +| 12 | distinct | 0.934 | 0.888 | +| 12 | repeated | 0.662 | 0.958 | +| 12 | result alias | 1.136 | 0.838 | +| 64 | distinct | 8.496 | 3.901 | +| 64 | repeated | 6.669 | 4.291 | +| 64 | result alias | 13.094 | 3.921 | +| 256 | distinct | 93.891 | 13.239 | +| 256 | repeated | 76.150 | 14.271 | +| 256 | result alias | 166.132 | 13.393 | + +At arity 12, distinct factors took 8,189 search nodes for both implementations. +Repeated factors took 1 versus 3 nodes; result aliases took 4,097 versus 4,117. +All paired exhaustive runs returned the same solution counts. + +The quadratic scans are visible at larger arities. A future optimization +should group repeated views once per filtering pass and reuse prefix/suffix +intervals before considering persistent caches. This sample does not measure +modular-product scaling, general signed domains, or application-level speed, +and does not establish a statistically robust performance guarantee. +No optimization was made on the basis of these timings alone. + +The small [C++ driver](../misc/bench-number-theory.cpp) and +[Python runner](../misc/bench-number-theory.py) preserve the experiment. +Compile the driver against a Release Gecode build using C++17 and optimization, +then run `python3 misc/bench-number-theory.py /path/to/driver`. +The CSV's zero node/solution fields at arities above 12 mean search was skipped. + +## Scope deliberately left out + +MiniModel expressions, FlatZinc/MiniZinc exposure, and a non-reified +`divides` overload were excluded by the feature brief. They are possible +future API work, not incomplete deliverables for this PR. Likewise, broad +performance optimization is a follow-up, not a prerequisite for the documented +semantics of these additive constraints. diff --git a/misc/bench-number-theory.cpp b/misc/bench-number-theory.cpp new file mode 100644 index 0000000000..e003290389 --- /dev/null +++ b/misc/bench-number-theory.cpp @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Mikael Zayenz Lagerkvist +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +using namespace Gecode; +class Model : public Space { +public: + IntVarArray x; + Model(int n, const std::string& pattern, bool decomposition, bool search) + : x(*this,n,0,1) { + if (pattern == "repeated") + for (int i=1; i + (std::chrono::steady_clock::now()-start).count()/repeats; + unsigned long nodes=0, solutions=0; + if (n<=12) { + Model m(n,pattern,decomposition,true); + DFS engine(&m); + while (Model* s=engine.next()) { solutions++; delete s; } + nodes=engine.statistics().node; + } + std::cout << us << ',' << nodes << ',' << solutions << '\n'; +} diff --git a/misc/bench-number-theory.py b/misc/bench-number-theory.py new file mode 100644 index 0000000000..dfddc501c5 --- /dev/null +++ b/misc/bench-number-theory.py @@ -0,0 +1,22 @@ +# Copyright (c) 2026 Mikael Zayenz Lagerkvist +# SPDX-License-Identifier: MIT +import sys +import statistics +import subprocess + +if len(sys.argv) != 2: + raise SystemExit("usage: bench-number-theory.py BENCHMARK_EXECUTABLE") + +print("arity,pattern,implementation,median_us,nodes,solutions") +for n in (4,12,64,256): + for pattern in ("distinct", "repeated", "alias"): + solutions = [] + for implementation in ("product", "chain"): + samples = [subprocess.check_output( + [sys.argv[1], str(n), pattern, implementation, "500"], + text=True, timeout=60).strip().split(",") for _ in range(3)] + solutions.append(samples[0][2]) + print(n, pattern, implementation, + round(statistics.median(float(s[0]) for s in samples), 3), + samples[0][1], samples[0][2], sep=",") + assert solutions[0] == solutions[1] From 0b69d3705df0abc480ea74ac2d06d303da62369b Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 8 Sep 2026 22:50:35 +0200 Subject: [PATCH 26/35] Build audit test target and include number theory in CI checks --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 4 ++++ Makefile.in | 2 ++ docs/gcd-pr-readiness.md | 5 ++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d28ad277a..1a813249ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,7 +249,7 @@ jobs: - name: Build Debug audit smoke shell: bash - run: cmake --build "$GITHUB_WORKSPACE/build-ninja-debug" + run: cmake --build "$GITHUB_WORKSPACE/build-ninja-debug" --target gecode-test - name: Run Debug audit arithmetic tests shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d0600f6b..0f0c8815eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1422,6 +1422,10 @@ if(BUILD_TESTING) Int::Arithmetic::Abs Int::Arithmetic::ArgMax Int::Arithmetic::Max::Nary + Int::Arithmetic::Gcd + Int::Arithmetic::Divides + Int::Arithmetic::Product + Int::Arithmetic::NumberTheory Int::Cumulative::Man::Fix::0::4 Int::Distinct::Random Int::Extensional::TupleSet::Sparse::IncrementalDelta diff --git a/Makefile.in b/Makefile.in index 2c67889a1f..1846825511 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1346,6 +1346,8 @@ test: mkcompiledirs $(BLACKBOXFIXTURES) @$(MAKE) $(VARIMP) $(TESTEXE) CHECKTESTS = Branch::Int::Dense::3 \ + Int::Arithmetic::Gcd Int::Arithmetic::Divides \ + Int::Arithmetic::Product Int::Arithmetic::NumberTheory \ FlatZinc::magic_square \ Int::Arithmetic::Abs \ Int::Arithmetic::ArgMax \ diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index ac4bb65ca7..133d2f40be 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -33,7 +33,10 @@ remain a release-wide responsibility. Positivity remains part of the proposition; the tests check that distinction. - Audited UBSan testing exposed pre-existing overflow in the `DivMod` and `Mod` test oracles. Their arithmetic now uses signed 64-bit intermediates. -- The existing Debug audit CI job now executes its arithmetic tests. +- CMake and Autoconf smoke selections now execute the new arithmetic families. + The Debug audit CI job explicitly builds the excluded-from-all test target + before executing its arithmetic tests. The initial CI attempt caught this + missing build target (exit 127); no arithmetic test had run in that step. - Added a [modeling example](integer-number-theory.md) covering signs, Euclidean residues, divisibility, and implication semantics. From 778ec06446ef67d7954e93c2b8696ec4e05433db Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 8 Sep 2026 22:57:26 +0200 Subject: [PATCH 27/35] Run propagator checks directly through Gecode test framework --- .github/workflows/build.yml | 27 +++++++++++++++++++++++++-- CMakeLists.txt | 4 ---- docs/gcd-pr-readiness.md | 6 +++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a813249ab..677a04438a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -148,6 +148,14 @@ jobs: set -euxo pipefail cmake --build . --config $BUILD_TYPE --target check + - name: Run number theory tests + shell: bash + run: > + "$GITHUB_WORKSPACE/build/bin/gecode-test" + -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' + -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' + -iter 2 -fixprob 1 -threads 2 + - name: Install CMake package working-directory: ${{github.workspace}}/build shell: bash @@ -231,8 +239,7 @@ jobs: - name: Check shell: bash run: > - ctest --test-dir "$GITHUB_WORKSPACE/build-ninja" - --output-on-failure -R '^(build-gecode-test|test)$' + cmake --build "$GITHUB_WORKSPACE/build-ninja" --target check - name: Configure CMake Debug audit smoke shell: bash @@ -411,6 +418,14 @@ jobs: shell: pwsh run: cmake --build "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --target check + - name: Run number theory tests + shell: pwsh + run: > + & "$env:GITHUB_WORKSPACE\build\bin\$env:BUILD_TYPE\gecode-test.exe" + -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' + -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' + -iter 2 -fixprob 1 -threads 2 + - name: Install CMake package shell: pwsh run: cmake --install "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --prefix "${{ github.workspace }}\\install" @@ -462,6 +477,14 @@ jobs: shell: pwsh run: cmake --build --preset vs2022-vcpkg-check + - name: Run number theory tests + shell: pwsh + run: > + & "$env:GITHUB_WORKSPACE\build\vs2022-vcpkg\bin\$env:BUILD_TYPE\gecode-test.exe" + -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' + -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' + -iter 2 -fixprob 1 -threads 2 + - name: Install CMake package shell: pwsh run: cmake --install "${{ github.workspace }}\\build\\vs2022-vcpkg" --config ${{ env.BUILD_TYPE }} --prefix "${{ github.workspace }}\\install-vcpkg" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f0c8815eb..e9d0600f6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1422,10 +1422,6 @@ if(BUILD_TESTING) Int::Arithmetic::Abs Int::Arithmetic::ArgMax Int::Arithmetic::Max::Nary - Int::Arithmetic::Gcd - Int::Arithmetic::Divides - Int::Arithmetic::Product - Int::Arithmetic::NumberTheory Int::Cumulative::Man::Fix::0::4 Int::Distinct::Random Int::Extensional::TupleSet::Sparse::IncrementalDelta diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index 133d2f40be..b9591ac4d7 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -33,7 +33,11 @@ remain a release-wide responsibility. Positivity remains part of the proposition; the tests check that distinction. - Audited UBSan testing exposed pre-existing overflow in the `DivMod` and `Mod` test oracles. Their arithmetic now uses signed 64-bit intermediates. -- CMake and Autoconf smoke selections now execute the new arithmetic families. +- All propagator tests live in Gecode's testing framework in + `test/int/arithmetic.cpp`. CI invokes `gecode-test` directly for the new + arithmetic families; Autoconf's `make check` also calls that executable. + No propagator tests or selectors are added to CTest. The Ninja check step + now uses the existing direct `check` build target instead of CTest. The Debug audit CI job explicitly builds the excluded-from-all test target before executing its arithmetic tests. The initial CI attempt caught this missing build target (exit 127); no arithmetic test had run in that step. From 829b1a65ad7651b8072f968fc259c11f4bafa664 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 9 Sep 2026 05:47:48 +0200 Subject: [PATCH 28/35] Use normal test integration for integer arithmetic propagators --- .github/workflows/build.yml | 35 +++-------------------------------- Makefile.in | 2 -- docs/gcd-pr-readiness.md | 10 +++------- 3 files changed, 6 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 677a04438a..b8457a5e7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -148,14 +148,6 @@ jobs: set -euxo pipefail cmake --build . --config $BUILD_TYPE --target check - - name: Run number theory tests - shell: bash - run: > - "$GITHUB_WORKSPACE/build/bin/gecode-test" - -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' - -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' - -iter 2 -fixprob 1 -threads 2 - - name: Install CMake package working-directory: ${{github.workspace}}/build shell: bash @@ -239,7 +231,8 @@ jobs: - name: Check shell: bash run: > - cmake --build "$GITHUB_WORKSPACE/build-ninja" --target check + ctest --test-dir "$GITHUB_WORKSPACE/build-ninja" + --output-on-failure -R '^(build-gecode-test|test)$' - name: Configure CMake Debug audit smoke shell: bash @@ -256,13 +249,7 @@ jobs: - name: Build Debug audit smoke shell: bash - run: cmake --build "$GITHUB_WORKSPACE/build-ninja-debug" --target gecode-test - - - name: Run Debug audit arithmetic tests - shell: bash - run: > - "$GITHUB_WORKSPACE/build-ninja-debug/bin/gecode-test" - -test '^Int::Arithmetic::' -iter 1 -seed 650 -threads 2 + run: cmake --build "$GITHUB_WORKSPACE/build-ninja-debug" - name: Install and smoke test FlatZinc tools shell: bash @@ -418,14 +405,6 @@ jobs: shell: pwsh run: cmake --build "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --target check - - name: Run number theory tests - shell: pwsh - run: > - & "$env:GITHUB_WORKSPACE\build\bin\$env:BUILD_TYPE\gecode-test.exe" - -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' - -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' - -iter 2 -fixprob 1 -threads 2 - - name: Install CMake package shell: pwsh run: cmake --install "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --prefix "${{ github.workspace }}\\install" @@ -477,14 +456,6 @@ jobs: shell: pwsh run: cmake --build --preset vs2022-vcpkg-check - - name: Run number theory tests - shell: pwsh - run: > - & "$env:GITHUB_WORKSPACE\build\vs2022-vcpkg\bin\$env:BUILD_TYPE\gecode-test.exe" - -test '^Int::Arithmetic::Gcd' -test '^Int::Arithmetic::Divides' - -test '^Int::Arithmetic::Product' -test '^Int::Arithmetic::NumberTheory' - -iter 2 -fixprob 1 -threads 2 - - name: Install CMake package shell: pwsh run: cmake --install "${{ github.workspace }}\\build\\vs2022-vcpkg" --config ${{ env.BUILD_TYPE }} --prefix "${{ github.workspace }}\\install-vcpkg" diff --git a/Makefile.in b/Makefile.in index 1846825511..2c67889a1f 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1346,8 +1346,6 @@ test: mkcompiledirs $(BLACKBOXFIXTURES) @$(MAKE) $(VARIMP) $(TESTEXE) CHECKTESTS = Branch::Int::Dense::3 \ - Int::Arithmetic::Gcd Int::Arithmetic::Divides \ - Int::Arithmetic::Product Int::Arithmetic::NumberTheory \ FlatZinc::magic_square \ Int::Arithmetic::Abs \ Int::Arithmetic::ArgMax \ diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md index b9591ac4d7..9ac092c070 100644 --- a/docs/gcd-pr-readiness.md +++ b/docs/gcd-pr-readiness.md @@ -34,13 +34,9 @@ remain a release-wide responsibility. - Audited UBSan testing exposed pre-existing overflow in the `DivMod` and `Mod` test oracles. Their arithmetic now uses signed 64-bit intermediates. - All propagator tests live in Gecode's testing framework in - `test/int/arithmetic.cpp`. CI invokes `gecode-test` directly for the new - arithmetic families; Autoconf's `make check` also calls that executable. - No propagator tests or selectors are added to CTest. The Ninja check step - now uses the existing direct `check` build target instead of CTest. - The Debug audit CI job explicitly builds the excluded-from-all test target - before executing its arithmetic tests. The initial CI attempt caught this - missing build target (exit 127); no arithmetic test had run in that step. + `test/int/arithmetic.cpp`, registered like other propagator tests. + There are no family-specific CI steps, CTest entries, or additions to + `make check`; the normal test suite handles these propagators. - Added a [modeling example](integer-number-theory.md) covering signs, Euclidean residues, divisibility, and implication semantics. From e671332095e570cd2ba1a12ed67782465eaf335d Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Wed, 9 Sep 2026 09:23:45 +0200 Subject: [PATCH 29/35] Remove development records before squash merge --- .zd/gcd/TASKS.md | 24 --- .zd/gcd/area.toml | 6 - .zd/gcd/brief.md | 163 ------------------ ...ment-the-ternary-integer-gcd-constraint.md | 46 ----- ...-the-reified-integer-divides-constraint.md | 45 ----- ...y-and-reified-n-ary-product-constraints.md | 45 ----- ...d-reified-fixed-modulus-n-ary-product-c.md | 46 ----- ...-support-enumeration-with-bounds-propag.md | 45 ----- ...-ary-product-mod-with-an-intvar-modulus.md | 47 ----- ...-ary-product-mod-with-an-intvar-modulus.md | 46 ----- ...-aware-inverse-bounds-for-n-ary-product.md | 45 ----- ...nit-and-sign-rewrites-for-n-ary-product.md | 45 ----- ...nd-result-alias-rewrites-for-n-ary-prod.md | 45 ----- ...ll-modulus-propagation-for-fixed-produc.md | 46 ----- ...ic-propagation-for-variable-product-mod.md | 47 ----- ...meration-from-number-theoretic-propagat.md | 48 ------ 16 files changed, 789 deletions(-) delete mode 100644 .zd/gcd/TASKS.md delete mode 100644 .zd/gcd/area.toml delete mode 100644 .zd/gcd/brief.md delete mode 100644 .zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md delete mode 100644 .zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md delete mode 100644 .zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md delete mode 100644 .zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md delete mode 100644 .zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md delete mode 100644 .zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md delete mode 100644 .zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md delete mode 100644 .zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md delete mode 100644 .zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md delete mode 100644 .zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md delete mode 100644 .zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md delete mode 100644 .zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md delete mode 100644 .zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md diff --git a/.zd/gcd/TASKS.md b/.zd/gcd/TASKS.md deleted file mode 100644 index cc1a76b475..0000000000 --- a/.zd/gcd/TASKS.md +++ /dev/null @@ -1,24 +0,0 @@ - - -# Tasks: gcd - -- Total: 13 -- Ready: 0 -- Blocked: 0 -- Done: 13 - -| ID | Task | State | Blocked by | -| --- | --- | --- | --- | -| [gcd-001](tasks/001-implement-the-ternary-integer-gcd-constraint.md) | Implement ternary and reified integer GCD constraints | done | — | -| [gcd-002](tasks/002-implement-the-reified-integer-divides-constraint.md) | Implement the reified integer divides constraint | done | — | -| [gcd-003](tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md) | Implement ordinary and reified n-ary product constraints | done | — | -| [gcd-004](tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md) | Implement ordinary and reified fixed-modulus n-ary product constraints | done | — | -| [gcd-005](tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md) | Replace n-ary product support enumeration with bounds propagation | done | — | -| [gcd-006](tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md) | Implement n-ary product_mod with an IntVar modulus | done | — | -| [gcd-007](tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md) | Implement reified n-ary product_mod with an IntVar modulus | done | gcd-006 | -| [gcd-008](tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md) | Complete zero-aware inverse bounds for n-ary product | done | — | -| [gcd-009](tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md) | Add zero, unit, and sign rewrites for n-ary product | done | — | -| [gcd-010](tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md) | Add repeated-factor and result-alias rewrites for n-ary product | done | gcd-009 | -| [gcd-011](tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md) | Add algebraic and bounds propagation for fixed product_mod | done | — | -| [gcd-012](tasks/012-add-algebraic-propagation-for-variable-product-mod.md) | Add algebraic propagation for variable product_mod | done | — | -| [gcd-013](tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md) | Eliminate support enumeration from number-theoretic propagators | done | gcd-008, gcd-009, gcd-010, gcd-011, gcd-012 | diff --git a/.zd/gcd/area.toml b/.zd/gcd/area.toml deleted file mode 100644 index 6db75d1d40..0000000000 --- a/.zd/gcd/area.toml +++ /dev/null @@ -1,6 +0,0 @@ -schema_version = 1 -tag = "gcd" -title = "Integer number-theoretic and product propagators" -objective = "Expose ordinary and reified integer constraints for GCD, divisibility, n-ary product, and fixed- or variable-modulus n-ary product, with sound propagation, public APIs, and focused regression coverage." -branch = "feature/gcd" -base_commit = "e1ec3da365f6c927d937b5ae9266d892a1cb9807" diff --git a/.zd/gcd/brief.md b/.zd/gcd/brief.md deleted file mode 100644 index c6f51e31a5..0000000000 --- a/.zd/gcd/brief.md +++ /dev/null @@ -1,163 +0,0 @@ -# Integer number-theoretic and product propagators - -## Objective - -Expose ordinary and reified integer constraints for GCD, divisibility, n-ary -product, and fixed- or variable-modulus n-ary product, with sound propagation, -public APIs, and focused regression coverage. - -## Success - -- The public integer API can post `gcd(home, x, y, z, ipl)` for integer - variables and constrains `z` to the mathematical greatest common divisor of - `x` and `y`. -- The GCD relation also has a `Reify` overload supporting `RM_EQV`, `RM_IMP`, - and `RM_PMI` for the proposition `z = gcd(x,y)`. -- The public integer API can post - `divides(home, divisor, dividend, reify, ipl)` and supports `RM_EQV`, - `RM_IMP`, and `RM_PMI` with the same meaning as existing reified integer - constraints. -- The public integer API can post ordinary and reified - `product(home, factors, result, ..., ipl)` constraints for an n-ary exact - product. -- The public integer API can post ordinary and reified - `product_mod(home, factors, modulus, result, ..., ipl)` constraints for a - fixed positive integer modulus and canonical Euclidean residue. -- The public integer API can also post ordinary and reified `product_mod` - constraints whose positive modulus is an integer variable. -- All constraints propagate soundly across negative, zero, sparse, assigned, - empty, repeated, and aliased variable domains as applicable; their actors - clone, subscribe, reschedule, rewrite, and subsume according to existing - Gecode conventions. -- Exact product propagation exploits strict and non-strict sign classes, - zero-aware inverse bounds, assigned units, repeated factors, powers, and - result aliases rather than treating every factor occurrence independently. -- Product-mod propagation exploits algebraic cases that remain useful beyond - capped Cartesian enumeration, including zero factors, factors equal to the - modulus, small fixed moduli, empty products, and divisor reasoning for an - assigned exact product or product-result difference. -- Focused integer tests validate solutions and propagation through the existing - test harness. - -## Boundaries - -- Implement integer-variable propagators and their public posting APIs. -- Keep the work in the integer arithmetic constraint family and follow its - existing header/source/build organization. -- Do not add MiniModel expressions, FlatZinc builtins, or unrelated arithmetic - constraints. -- Do not introduce a new test harness or broad solver-wide test matrix. -- Only the reified `divides` API is in scope; a separate non-reified overload is - not required. -- Variable-modulus `product_mod` accepts one `IntVar` modulus; MiniModel and - FlatZinc exposure remain outside scope. -- Gecode has no proof-logging subsystem. Proof encodings, proof certificates, - and proof-logging APIs are outside scope; multiplication proof-logging work - may motivate mathematical case analysis but not implementation artifacts. - -## Terms - -- `gcd(x,y)` is the unique nonnegative greatest common divisor. Negative - operands are interpreted by absolute value, `gcd(0,n)=abs(n)`, and - `gcd(0,0)=0`. -- `divisor divides dividend` means there exists an integer `k` such that - `dividend = divisor*k`. Consequently, zero divides zero, while zero does not - divide a nonzero integer. -- `product(x)` is the exact mathematical product of all variables in `x`, with - the empty product equal to one. Implementations must not silently overflow - while computing supports or bounds. -- `product_mod(x,m)` is the unique residue `r` such that - `m > 0`, `r` is congruent to `product(x)` modulo `m`, and - `0 <= r < m`. Thus `product_mod([],m) = 1 mod m`, including zero when `m=1`. -- Reification follows Gecode's `Reify` modes: equivalence (`RM_EQV`), forward - implication (`RM_IMP`), and reverse implication (`RM_PMI`). -- Multiplication sign classes distinguish zero, positive, nonnegative, - negative, nonpositive, and mixed domains. Strict sign classes exclude zero; - non-strict classes retain zero as a semantically important case. - -## Decisions - -- Use the mathematical zero semantics selected by the user rather than treating - a zero divisor as unconditionally invalid. -- Expose `IntPropLevel` consistently with neighboring integer arithmetic APIs; - implementations may map unsupported strengths to the strongest sound level - they actually provide, as existing APIs do. -- Prefer dedicated propagators and standard Gecode view/actor base classes over - a decomposition into multiplication, modulo, or extensional constraints. -- Use SMT-LIB's Euclidean remainder convention for `product_mod`, specialized - to a fixed positive modulus, rather than Gecode's existing dividend-signed - `mod` convention. -- Pair each ordinary n-ary product relation with a reified overload; reified - GCD means reifying the full proposition `z = gcd(x,y)`. -- Propagate the positive n-ary exact-product relation from variable bounds in - general, without making useful propagation depend on Cartesian support - enumeration or a small-domain tuple threshold. -- For a variable modulus, positivity is part of the `product_mod` relation. - The ordinary constraint enforces it. Reification controls the full - proposition, so inactive implications do not narrow the modulus or result. -- Exact-product inverse propagation follows the complete zero-aware quotient - cases used by Gecode-style multiplication: no filtering when cofactor and - result both contain zero, failure for a zero cofactor with a nonzero result, - magnitude filtering when the cofactor spans zero but the result does not, - and directed floor/ceiling division for zero-free or one-sided cofactors. -- Product-mod specialisation uses modular algebra rather than ordinary-product - sign monotonicity, which does not survive Euclidean reduction. -- None of the propagators introduced in this area use Cartesian domain-tuple - enumeration, support tables, or projected tuple supports as a propagation - strategy. Prefer bounds, sign and zero classification, divisibility, - congruence, and algebraic rewrites. A fully assigned relation may be - evaluated directly; reified relations otherwise remain conservative when - entailment or disentailment cannot be established algebraically. -- In particular, ordinary and reified GCD and reified divides must not use - small-domain enumeration as a stronger propagation level. Their propagation - and reified status use bounds, zero/sign cases, divisibility, gcd identities, - and conservative entailment or disentailment when algebra cannot decide. -- Computing a single mathematically determined output from assigned inputs is - algebraic propagation and is permitted; enumerating combinations of - unassigned domains or projecting their supports is not. -- Divisibility and congruence propagation should prefer interval tightening - and algebraic rewrites. Do not materialize a large sparse support set or - punch many holes into an integer domain merely because exact divisors or - residues can be generated; retain conservative bounds and perform exact - checking at assignment unless interior removal has a clear, bounded benefit. -- Prefer bounds subscriptions for bounds propagators even when an interior - domain change, such as removal of zero, could enable stronger pruning. - Missing that wakeup is acceptable weak monotonicity; do not subscribe to all - domain changes solely to observe it. Use the repository's weakly monotonic - execution-status convention so propagation remains lifecycle-correct. - -## Open questions - -None. - -## Testing - -Use focused coverage in the existing `test/int/arithmetic.cpp` framework. Add -solution and propagation tests for small dense and sparse domains containing -negative values and zero, assigned inputs, variable aliasing, and cloning. For -`divides`, exercise all three reification modes and fixed/unfixed Boolean -controls, including the selected `0 | 0` behavior. For `gcd`, cover sign -normalization, the `(0,0)` case, and all reification modes. For `product`, cover -empty and singleton arrays, zeros, signs, repeated or aliased factors, result -aliasing, exact-product overflow boundaries, and all reification modes. For -`product_mod`, cover modulus one, negative factors, canonical residues, empty -and singleton arrays, aliasing, and all reification modes. Variable-modulus -coverage also includes nonpositive candidates, sparse modulus domains, -modulus/result or modulus/factor aliasing, and inactive implication behavior. -Focused propagation cases additionally cover all strict and non-strict sign -classes, zero-containing cofactors, assigned zero/one/minus-one factors, -repeated factors, result aliases, and large-domain algebraic pruning that does -not depend on Cartesian enumeration or support projection. Include cases above -the former 200,000-tuple cutoff so useful behavior and fully assigned checking -cannot accidentally depend on that threshold. -Reuse existing arithmetic test helpers and commands; do not create new test -infrastructure or an exhaustive large-domain performance matrix. - -## Validation - -- Build the affected integer library and test target with the repository's - established build configuration. -- Run the focused integer arithmetic tests. -- Run the broader integer test target if the established local build exposes it - without requiring a new configuration. -- Run `zd check gcd --format json` after task-state changes. diff --git a/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md b/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md deleted file mode 100644 index 2789560599..0000000000 --- a/.zd/gcd/tasks/001-implement-the-ternary-integer-gcd-constraint.md +++ /dev/null @@ -1,46 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-001" -key = "ternary-gcd" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Implement ternary and reified integer GCD constraints - -## Outcome - -Users can post ordinary and reified ternary integer constraints for z = gcd(x, y) with the mathematical sign and zero semantics recorded in the brief. - -## Boundaries - -- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. -- Do not implement the reified divides constraint in this task. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] Documented public gcd posting APIs for the ordinary relation and its Reify overload are available from gecode/int.hh and dispatch through the integer arithmetic implementation. -- [x] A dedicated ternary propagator implements sound posting, propagation, cloning, subscription, rescheduling, and subsumption for negative, zero, sparse, assigned, and aliased domains. -- [x] The reified GCD relation implements sound Boolean control, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI semantics for the full proposition z = gcd(x,y). -- [x] The result is constrained to the unique nonnegative gcd, including gcd(0,0)=0 and gcd(0,n)=abs(n). -- [x] Focused arithmetic tests cover ordinary and reified solution semantics, all reification modes, fixed and unfixed controls, propagation, sign normalization, zero cases, sparse domains, aliasing, and cloning without introducing new test infrastructure. -- [x] The affected integer library and focused tests build successfully. - -## Validation - -- Build the configured integer library and test executable. -- Run the focused ordinary and reified Int::Arithmetic::Gcd tests. -- Run the broader integer test target when available in the existing local build. - -## Result - -Implemented and independently verified ordinary and reified ternary integer GCD constraints with mathematical sign and zero semantics, alias-aware support filtering, and sound large-domain fallback. - -Validation: - -- Built gecodeint_shared and gecode-test successfully. -- Focused Arithmetic::Gcd tests passed all 15 variants. -- Broader Int::Arithmetic tests passed. -- Traditional header-list and staged CMake install checks included arithmetic/gcd.hpp. -- git diff --check passed. diff --git a/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md b/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md deleted file mode 100644 index b43dc13e72..0000000000 --- a/.zd/gcd/tasks/002-implement-the-reified-integer-divides-constraint.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-002" -key = "reified-divides" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Implement the reified integer divides constraint - -## Outcome - -Users can reify the relation divisor divides dividend between two integer variables in every Gecode reification mode, using the mathematical zero semantics recorded in the brief. - -## Boundaries - -- Expose only the reified divides API requested in the brief; do not add a separate non-reified overload. -- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. -- Follow the brief's focused testing level and existing reified integer-test patterns. - -## Done when - -- [x] The documented public divides posting API accepts two IntVar operands, Reify, and IntPropLevel and is available from gecode/int.hh. -- [x] A dedicated reified propagator implements sound posting, propagation, cloning, subscription, rescheduling, rewriting or subsumption as appropriate, and Boolean control for RM_EQV, RM_IMP, and RM_PMI. -- [x] The relation uses existential integer-multiplier semantics, including true for 0 divides 0 and false for 0 dividing any nonzero value. -- [x] Focused arithmetic tests cover all reification modes, fixed and unfixed controls, true and false relations, negative and zero values, sparse domains, aliasing, and cloning without introducing new test infrastructure. -- [x] The affected integer library and focused tests build successfully. - -## Validation - -- Build the configured integer library and test executable. -- Run the focused Int::Arithmetic::Divides tests. -- Run the broader integer test target when available in the existing local build. - -## Result - -Implemented and independently verified the reified integer divides relation for all reification modes with existential multiplier semantics, signed operands, zero semantics, aliases, and sound large-domain monitoring. - -Validation: - -- Built gecodeint_shared and gecode-test successfully. -- Focused Arithmetic::Divides tests passed all nine groups for five iterations. -- Broader Int::Arithmetic tests passed. -- Fresh CMake install included arithmetic/divides.hpp. -- git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md b/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md deleted file mode 100644 index 5d1a853c2e..0000000000 --- a/.zd/gcd/tasks/003-implement-ordinary-and-reified-n-ary-product-constraints.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-003" -key = "nary-product" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Implement ordinary and reified n-ary product constraints - -## Outcome - -Users can constrain an integer result to the exact product of an IntVarArgs array, ordinarily or through any Gecode reification mode. - -## Boundaries - -- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. -- Implement exact product only; fixed-modulus product belongs to its own task. -- Follow the brief's focused testing level and existing arithmetic and reification test patterns. - -## Done when - -- [x] Documented public ordinary and Reify product posting APIs accept IntVarArgs factors, an IntVar result, and IntPropLevel and are available from gecode/int.hh. -- [x] Dedicated n-ary actors implement sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. -- [x] The relation uses exact mathematical multiplication, defines the empty product as one, handles singleton, repeated, zero, signed, sparse, assigned, and aliased variables, and never silently overflows internal arithmetic. -- [x] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, empty and singleton arrays, zeros, signs, sparse domains, repetition, result aliasing, cloning, and representable-limit cases without new test infrastructure. -- [x] The affected integer library and focused tests build successfully. - -## Validation - -- Build the configured integer library and test executable. -- Run the focused ordinary and reified Int::Arithmetic::Product tests. -- Run the broader integer test target when available in the existing local build. - -## Result - -Implemented and independently verified ordinary and reified exact n-ary product constraints with empty-product semantics, alias-aware support filtering, checked arithmetic, and sound large-domain fallback. - -Validation: - -- Built gecodeint_shared and gecode-test successfully. -- All 18 focused Int::Arithmetic::Product cases passed. -- Broader Int::Arithmetic suite passed. -- Fresh install and installed-consumer smoke tests passed, including large-domain assignment and cloning. -- git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md b/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md deleted file mode 100644 index f24d2d7621..0000000000 --- a/.zd/gcd/tasks/004-implement-ordinary-and-reified-fixed-modulus-n-ary-product-c.md +++ /dev/null @@ -1,46 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-004" -key = "nary-product-mod" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Implement ordinary and reified fixed-modulus n-ary product constraints - -## Outcome - -Users can constrain an integer result to the canonical Euclidean residue of an IntVarArgs product under a fixed positive modulus, ordinarily or through any Gecode reification mode. - -## Boundaries - -- Accept a fixed positive int modulus only; do not add an IntVar modulus overload. -- Keep this slice within the integer arithmetic API and propagator implementation; do not add MiniModel or FlatZinc support. -- Follow the brief's focused testing level and existing arithmetic and reification test patterns. - -## Done when - -- [x] Documented public ordinary and Reify product_mod posting APIs accept IntVarArgs factors, a fixed int modulus, an IntVar result, and IntPropLevel and are available from gecode/int.hh. -- [x] Posting rejects a nonpositive modulus through the repository's established integer-argument exception convention and constrains every result to the canonical range zero through modulus minus one. -- [x] Dedicated n-ary actors implement overflow-safe modular arithmetic, sound posting, propagation, cloning, disposal, subscription, rescheduling, rewriting or subsumption as appropriate, and RM_EQV, RM_IMP, and RM_PMI control. -- [x] The relation uses SMT-LIB-compatible Euclidean residues, defines the empty product as one modulo the modulus, and handles modulus one, singleton, repeated, zero, signed, sparse, assigned, and aliased variables. -- [x] Focused arithmetic tests cover ordinary and reified semantics, all reification modes, fixed and unfixed controls, modulus one, canonical residues for negative factors, empty and singleton arrays, zeros, sparse domains, repetition, result aliasing, and cloning without new test infrastructure. -- [x] The affected integer library and focused tests build successfully. - -## Validation - -- Build the configured integer library and test executable. -- Run the focused ordinary and reified Int::Arithmetic::ProductMod tests. -- Run the broader integer test target when available in the existing local build. - -## Result - -Implemented and independently verified ordinary and reified fixed-modulus n-ary product constraints with canonical Euclidean residues, positive-modulus validation, overflow-safe modular arithmetic, alias-aware support filtering, and correct inactive implication behavior. - -Validation: - -- Built the integer library and gecode-test successfully. -- All 19 focused Int::Arithmetic::ProductMod cases passed. -- All 473 broader Int::Arithmetic tests passed. -- Fresh install, exported-symbol/header assertions, and targeted implication/range/modulus-one/max-modulus consumer smokes passed. -- git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md b/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md deleted file mode 100644 index 26de2991c6..0000000000 --- a/.zd/gcd/tasks/005-replace-n-ary-product-support-enumeration-with-bounds-propag.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-005" -key = "product-bounds" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Replace n-ary product support enumeration with bounds propagation - -## Outcome - -The positive n-ary exact-product relation performs sound, useful bounds propagation for general integer domains without enumerating Cartesian supports. - -## Boundaries - -- Keep the public product API and exact mathematical semantics unchanged. -- Do not change product_mod or unrelated arithmetic propagators. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] The ordinary Product actor derives sound bounds for the result and factors using overflow-safe interval arithmetic rather than Cartesian support enumeration. -- [x] Positive reified product cases rewrite to or otherwise use the same bounds propagator, while negative and undecided reification modes remain sound. -- [x] Subscriptions, propagation conditions, costs, fixpoint status, cloning, aliasing, zero crossing, signs, and representable-limit behavior follow Gecode actor conventions. -- [x] Focused regression tests demonstrate useful bounds pruning on domains whose Cartesian size exceeds the former support-enumeration threshold. -- [x] The affected integer library and focused product tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run the focused Int::Arithmetic::Product tests with multiple iterations. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Replaced exact n-ary product Cartesian support enumeration with independently verified overflow-safe forward and backward bounds propagation. - -Validation: - -- Built gecodeint_shared and gecode-test successfully. -- All 19 exact Int::Arithmetic::Product cases passed for five iterations. -- Broader Int::Arithmetic tests and targeted signed, zero-after-overflow, and nonrepresentable-product checks passed. -- git diff --check and zd check gcd passed. diff --git a/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md deleted file mode 100644 index 2fdfcb92d2..0000000000 --- a/.zd/gcd/tasks/006-implement-n-ary-product-mod-with-an-intvar-modulus.md +++ /dev/null @@ -1,47 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-006" -key = "variable-product-mod" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Implement n-ary product_mod with an IntVar modulus - -## Outcome - -Users can constrain an integer result to the canonical Euclidean residue of an n-ary product under a positive integer-variable modulus. - -## Boundaries - -- Add the ordinary IntVar-modulus overload without changing the existing fixed-int overload's behavior. -- Keep this slice within integer arithmetic APIs and propagators; do not add MiniModel or FlatZinc support. -- Leave reification to the dependent variable-product-mod-reified task. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] A documented public ordinary product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, and IntPropLevel. -- [x] The ordinary constraint enforces modulus > 0 and the canonical result range 0 <= result < modulus. -- [x] A dedicated or appropriately shared actor propagates soundly for assigned and unassigned moduli, negative and zero factors, empty and singleton products, sparse domains, repeated variables, aliases, modulus one, and representable limits. -- [x] Actor posting, subscriptions, propagation, cloning, rescheduling, disposal, rewriting, and subsumption follow existing Gecode conventions without unsafe arithmetic. -- [x] Focused existing-harness tests cover ordinary variable-modulus solutions and propagation, including nonpositive modulus removal and aliases. -- [x] The affected integer library and focused ProductMod tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run the focused ordinary variable-modulus ProductMod tests. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Implemented and independently verified ordinary n-ary product_mod with a positive IntVar modulus and canonical Euclidean result. - -Validation: - -- Built gecode-test and the affected integer library successfully. -- All 25 ProductModVar cases and all 19 fixed ProductMod regressions passed. -- Broader Int::Arithmetic tests, fresh install, and installed-consumer smoke passed. -- git diff --check and zd check gcd passed. diff --git a/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md b/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md deleted file mode 100644 index b996306c00..0000000000 --- a/.zd/gcd/tasks/007-implement-reified-n-ary-product-mod-with-an-intvar-modulus.md +++ /dev/null @@ -1,46 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-007" -key = "variable-product-mod-reified" -area = "gcd" -status = "done" -blocked_by = ["gcd-006"] -+++ -# Implement reified n-ary product_mod with an IntVar modulus - -## Outcome - -Users can reify the full positive-variable-modulus product_mod proposition in every Gecode reification mode. - -## Boundaries - -- Build on the ordinary variable-modulus relation and preserve both existing fixed-modulus overloads. -- Keep this slice within integer arithmetic APIs and propagators; do not add MiniModel or FlatZinc support. -- Follow the brief's focused testing level and existing reified arithmetic-test patterns. - -## Done when - -- [x] A documented public product_mod overload accepts IntVarArgs factors, an IntVar modulus, an IntVar result, Reify, and IntPropLevel. -- [x] The reified proposition includes modulus > 0, canonical range, and congruence, with correct RM_EQV, RM_IMP, and RM_PMI behavior. -- [x] Inactive implications do not narrow the modulus, factors, or result; positive control rewrites to or shares the ordinary variable-modulus propagator. -- [x] The actor handles fixed and unfixed Boolean controls, nonpositive modulus candidates, sparse domains, modulus one, empty and singleton products, signs, zeros, aliases, cloning, rewriting, and subsumption soundly. -- [x] Focused existing-harness tests cover all reification modes and fixed/unfixed controls, including inactive implications and false supports from nonpositive moduli or noncanonical results. -- [x] The affected integer library and focused ProductMod tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run the focused reified variable-modulus ProductMod tests with all reification modes. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Implemented and independently verified reified n-ary product_mod with an IntVar modulus for RM_EQV, RM_IMP, and RM_PMI. - -Validation: - -- Built gecode-test and the affected integer library successfully. -- All 26 ProductModVar cases and all 19 fixed ProductMod regressions passed. -- Broader Int::Arithmetic tests, fresh install, exported API checks, and installed-consumer reification smoke passed. -- git diff --check and zd check gcd passed. diff --git a/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md b/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md deleted file mode 100644 index 2e24d33088..0000000000 --- a/.zd/gcd/tasks/008-complete-zero-aware-inverse-bounds-for-n-ary-product.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-008" -key = "product-zero-aware-inverse-bounds" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Complete zero-aware inverse bounds for n-ary product - -## Outcome - -The exact n-ary Product actor performs sound backward bounds propagation for positive, nonnegative, negative, nonpositive, mixed, and zero cofactor domains. - -## Boundaries - -- Keep the exact-product public APIs and mathematical semantics unchanged. -- Do not add proof logging, proof encodings, or proof-only state. -- Do not change product_mod in this task. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] Backward factor propagation implements the full zero-aware quotient case split: no filter when cofactor and result both contain zero, failure when a zero cofactor cannot produce the result, magnitude bounds when a cofactor spans zero and the result excludes zero, and directed floor/ceiling quotient bounds for one-sided cofactors. -- [x] Positive, nonnegative, negative, nonpositive, mixed, and fixed-zero domain combinations produce sound bounds without division by zero or signed overflow. -- [x] Cofactor interval computation is structured to avoid unnecessary repeated whole-array recomputation while preserving safe saturation and alias behavior. -- [x] Ordinary propagation and positive reified rewrites reach an honest fixpoint with correct subscriptions, costs, cloning, and subsumption. -- [x] Focused tests exercise every sign/zero case, large domains, aliases, and overflow boundaries through the existing Product harness. -- [x] The affected integer library and focused Product tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run focused Int::Arithmetic::Product sign-class and inverse-bound tests with multiple iterations. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Added complete zero-aware inverse bounds for n-ary Product using prefix/suffix cofactor intervals and safe quotient hulls for all sign classes. - -Validation: - -- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. -- Independent spec and standards verification, exhaustive small-interval diagnostics, git diff --check, and zd check passed. diff --git a/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md b/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md deleted file mode 100644 index eb9f29850b..0000000000 --- a/.zd/gcd/tasks/009-add-zero-unit-and-sign-rewrites-for-n-ary-product.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-009" -key = "product-zero-unit-sign-rewrites" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Add zero, unit, and sign rewrites for n-ary product - -## Outcome - -Exact product posting and propagation simplify assigned zero, one, and minus-one factors and exploit stable aggregate sign information. - -## Boundaries - -- Keep repeated-variable powers and result-alias identities for the dependent alias-rewrites task. -- Do not add proof logging or change product_mod. -- Preserve all ordinary and reified API semantics, including inactive implications. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] A fixed-zero factor assigns the positive relation's result to zero; assigned one factors are removed; assigned minus-one factors are removed with the result sign transformed safely. -- [x] If the result excludes zero, bounds-visible zero endpoints are removed from nonnegative or nonpositive factors, and a zero result with exactly one possible zero source propagates that source to zero when sound. -- [x] Aggregate sign parity and zero possibility constrain the result sign and permit safe normalisation or rewriting to a positive-magnitude path when all remaining factors have strict signs. -- [x] Reified actors apply these deductions only when truth is required and preserve no-narrowing inactive implication behavior. -- [x] Focused tests cover zero, one, minus one, strict and non-strict sign combinations, sign parity, reification controls, cloning, and representable limits. -- [x] The affected integer library and focused Product tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run focused Int::Arithmetic::Product simplification and sign tests with multiple iterations. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Added zero/unit simplification, minus-unit parity, zero-source and aggregate-sign reasoning to Product using the bounds-subscribed weak-monotonic actor convention. - -Validation: - -- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. -- Independent spec/standards and explicit clone-disable-enable verification passed; git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md b/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md deleted file mode 100644 index f569c09b75..0000000000 --- a/.zd/gcd/tasks/010-add-repeated-factor-and-result-alias-rewrites-for-n-ary-prod.md +++ /dev/null @@ -1,45 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-010" -key = "product-alias-power-rewrites" -area = "gcd" -status = "done" -blocked_by = ["gcd-009"] -+++ -# Add repeated-factor and result-alias rewrites for n-ary product - -## Outcome - -Exact product propagation recognises repeated variables as powers and result aliases as algebraic identities instead of independent interval occurrences. - -## Boundaries - -- Build on the zero, unit, and sign simplification machinery from the blocking task. -- Do not add proof logging or change product_mod. -- Use existing Gecode power, square, view, and actor patterns where they preserve the direct n-ary overflow and zero semantics. -- Follow the brief's focused testing level and existing arithmetic-test patterns. - -## Done when - -- [x] Repeated occurrences of the same factor exploit even/odd power structure, including a nonnegative lower bound for even powers over mixed domains and sound inverse power bounds. -- [x] The implementation does not use an intermediate-product decomposition that rejects a valid final zero merely because an earlier partial product is nonrepresentable. -- [x] A result variable appearing among the factors is simplified using the identity result=0 or product(other factors)=1, with correct handling of multiple occurrences. -- [x] Aliased and repeated views remain sound under cloning, propagation, reification, overflow, and zero cases. -- [x] Focused tests demonstrate stronger pruning for squares/even powers, odd powers, repeated factors, single and multiple result aliases, signs, zeros, and reification modes. -- [x] The affected integer library and focused Product tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run focused Int::Arithmetic::Product alias and power tests with multiple iterations. -- Run the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Grouped repeated Product factors as powers with safe even/odd forward and inverse bounds, and added algebraic result-alias cancellation while preserving direct zero/overflow semantics. - -Validation: - -- Built gecodeint_shared and gecode-test; focused Product tests passed for five iterations and broader Int::Arithmetic passed. -- Independent verification plus 573,300 exhaustive small power/inverse checks and Limits root checks passed after correcting ReProduct cost; diff and zd checks passed. diff --git a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md b/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md deleted file mode 100644 index 825dcf7f70..0000000000 --- a/.zd/gcd/tasks/011-add-algebraic-and-small-modulus-propagation-for-fixed-produc.md +++ /dev/null @@ -1,46 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-011" -key = "product-mod-fixed-specialisation" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Add algebraic and bounds propagation for fixed product_mod - -## Outcome - -Fixed-modulus product_mod propagates important algebraic, interval, and congruence cases without enumerating domain tuples or projected supports. - -## Boundaries - -- Keep the fixed positive int modulus API and Euclidean residue semantics unchanged. -- Do not add proof logging or modify variable-modulus APIs except shared helpers that preserve behavior. -- Use bounds, quotient-band, divisibility, and congruence reasoning; do not build reachable-residue support tables or enumerate factor/result combinations. -- Preserve all reification modes and inactive implication behavior. - -## Done when - -- [x] Modulus one, a fixed-zero factor, and a factor fixed to a multiple of the modulus force result zero; empty and assigned unit factors simplify directly. -- [x] When exact-product bounds fit one quotient band, the relation rewrites or propagates as result=product-k*modulus, including the no-wrap case. -- [x] Fixed-modulus reasoning derives sound canonical-result and factor bounds from interval, quotient-band, divisibility, and congruence properties without constructing residue supports. -- [x] Domains for which those deductions are inconclusive retain a sound conservative path without domain-product thresholds, support allocation, or tuple enumeration. -- [x] Ordinary and reified focused tests cover algebraic cases, negative factors, wrap/no-wrap bands, sparse domains, aliases, controls, cloning, and large-domain behavior. -- [x] The affected integer library and focused ProductMod tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run focused fixed-modulus Int::Arithmetic::ProductMod tests with multiple iterations. -- Run variable-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Replaced fixed-modulus Cartesian support filtering with threshold-free algebraic bounds, quotient-band, inverse-bound, zero/unit, and congruence propagation, with sound reified status handling. - -Validation: - -- Built gecodeint_shared and gecode-test successfully. -- Fixed ProductMod tests passed for five iterations; ProductModVar regression and broader Int::Arithmetic passed. -- Independent spec and standards verification passed after correcting ProductMod scheduling cost; git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md b/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md deleted file mode 100644 index 14c11f3b47..0000000000 --- a/.zd/gcd/tasks/012-add-algebraic-propagation-for-variable-product-mod.md +++ /dev/null @@ -1,47 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-012" -key = "product-mod-variable-specialisation" -area = "gcd" -status = "done" -blocked_by = [] -+++ -# Add algebraic propagation for variable product_mod - -## Outcome - -Variable-modulus product_mod derives useful modulus and result information from identities and assigned products even when Cartesian support enumeration is capped. - -## Boundaries - -- Keep positivity as part of the full reified proposition and preserve inactive implication no-narrowing behavior. -- Do not add proof logging or change fixed-modulus semantics. -- Do not compute an overflowing exact product; use safe zero detection, checked arithmetic, or divisor reasoning only when the required difference is representable. -- Follow the brief's focused testing level and existing ProductModVar patterns. - -## Done when - -- [x] A factor aliased with the modulus forces canonical result zero in the positive relation, while modulus=result remains correctly impossible. -- [x] The empty product propagates the piecewise identity m=1 implies result=0 and m>=2 implies result=1, including backward pruning from the result. -- [x] For assigned factors and result, divisor reasoning tightens modulus bounds and detects absence of a feasible divisor without scanning the modulus domain or creating a large set of holes; product=result preserves every modulus above result. -- [x] A zero result exposes sound modulus-divides-product bounds where the product is safely known, without Cartesian enumeration or projecting every divisor into the modulus domain. -- [x] Reified false and unfixed paths treat invalid modulus/result combinations correctly and leave inactive implications untouched. -- [x] Focused tests cover empty products, assigned product/difference divisors, factor-modulus aliases, zero result, sparse and large modulus domains, all reification modes, cloning, and arithmetic limits. -- [x] The affected integer library and focused ProductModVar tests build and pass. - -## Validation - -- Build the configured integer library and gecode-test executable. -- Run focused Int::Arithmetic::ProductModVar algebraic tests with multiple iterations. -- Run fixed-modulus ProductMod regressions and the broader Int::Arithmetic tests when available. -- Run git diff --check and zd check gcd --format json. - -## Result - -Replaced variable-modulus Cartesian support filtering with algebraic empty/alias/status reasoning and bounds-only divisor extrema, avoiding dense-domain scans and mass hole creation. - -Validation: - -- Built the integer library and gecode-test; ProductModVar passed three focused iterations and fixed ProductMod regressions passed. -- Broader Int::Arithmetic passed; dense full-range modulus smoke completed in 0.01 seconds with sound bound propagation. -- Independent spec and standards verification passed; git diff --check and zd check passed. diff --git a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md b/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md deleted file mode 100644 index 2c237fd42c..0000000000 --- a/.zd/gcd/tasks/013-eliminate-support-enumeration-from-number-theoretic-propagat.md +++ /dev/null @@ -1,48 +0,0 @@ -+++ -schema_version = 1 -id = "gcd-013" -key = "eliminate-support-enumeration" -area = "gcd" -status = "done" -blocked_by = ["gcd-008", "gcd-009", "gcd-010", "gcd-011", "gcd-012"] -+++ -# Eliminate support enumeration from number-theoretic propagators - -## Outcome - -Every propagator added in the gcd area uses bounds or algebraic reasoning instead of Cartesian support enumeration for propagation and reification status. - -## Boundaries - -- Audit Gcd/ReGcd, ReDivides, Product/ReProduct, ProductMod/ReProductMod, and ProductModVar/ReProductModVar, including shared helpers. -- Gcd/ReGcd and ReDivides must remove their 200,000-tuple thresholds, Cartesian domain scans, support arrays, and projected filtering rather than retaining enumeration for small domains. -- Do not change the public relation semantics or add proof-logging artifacts. -- Direct evaluation of a complete relation is allowed when all relevant variables are assigned. Computing one determined output from assigned inputs is also allowed; do not enumerate combinations of unassigned domains or collect/project tuple supports. -- Use the existing integer arithmetic test harness and the focused testing level in the area brief. - -## Done when - -- [x] No audited propagator or helper enumerates a Cartesian product of variable domains, allocates tuple-support tables, or filters domains by projected tuple supports. -- [x] Gcd/ReGcd and ReDivides use bounds and number-theoretic identities for propagation and conservative algebraic reification status, with no small-domain enumeration fallback. -- [x] Ordinary propagation is expressed through sound bounds, sign/zero classification, divisibility, congruence, or algebraic rewrites, with conservative behavior where those deductions are inconclusive. -- [x] Reified entailment and disentailment use sound bounds or algebraic tests and exact evaluation only for fully assigned relations; negative or unfixed cases wait conservatively instead of enumerating supports. -- [x] Propagation conditions, subscriptions, and costs match the information actually used, including bounds subscriptions where interior-domain events are no longer required. -- [x] Focused tests exercise GCD, divides, product, fixed-modulus product_mod, and variable-modulus product_mod on domains above the former 200,000-tuple cutoff, relevant reification modes, and fully assigned true and false leaves. -- [x] The affected integer library, all focused new-propagator tests with multiple iterations, and the broader integer arithmetic suite build and pass. - -## Validation - -- Search the gcd-area implementation for support-table, Cartesian-enumeration, and tuple-projection machinery and inspect every remaining domain iterator. -- Build the configured integer library and gecode-test executable. -- Run focused Gcd, Divides, Product, ProductMod, and ProductModVar tests with multiple iterations. -- Run the broader Int::Arithmetic tests. -- Run git diff --check and zd check gcd --format json. - -## Result - -Removed all Cartesian/support enumeration and tuple cutoffs from GCD and divides, converted audited actors to bounds/algebraic conservative propagation with weak lifecycle where needed, and added above-cutoff staged leaf coverage for every new family. - -Validation: - -- Built gecodeint_shared and gecode-test; Gcd, Divides, Product, ProductMod, ProductModVar and large-leaf tests passed for three iterations; broader Int::Arithmetic passed. -- Independent spec/standards verification and static anti-enumeration audit passed; git diff --check and zd check passed. From 11cc8c3a68093a02a1876e07322a4b2330ec93a3 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 08:05:35 +0200 Subject: [PATCH 30/35] Reuse product grouping across bounds propagation --- gecode/int/arithmetic/product.hpp | 66 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index 70636341f8..c2c2edbae2 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -164,14 +164,18 @@ namespace Gecode { namespace Int { namespace Arithmetic { for (int i=0; i(x.size()); + int* exponent=r.alloc(x.size()); + int groups=0; + for (int i=0; i(groups); + ProductInterval* prefix=r.alloc(groups+1); + ProductInterval* suffix=r.alloc(groups+1); bool modified; do { modified = false; - - ProductInterval p = product_interval(x); + prefix[0] = ProductInterval{1,1}; + for (int g=0; g(x.size()); - int* exponent=r.alloc(x.size()); - int groups=0; - for (int i=0; i(groups); - ProductInterval* prefix = r.alloc(groups+1); - ProductInterval* suffix = r.alloc(groups+1); - prefix[0] = ProductInterval{1,1}; - for (int g=0; g Date: Thu, 10 Sep 2026 08:10:21 +0200 Subject: [PATCH 31/35] Rewrite small products using existing arithmetic and equality propagators --- gecode/int/arithmetic/product-mod.hpp | 22 ++---------------- gecode/int/arithmetic/product.hpp | 32 +++++++++++++-------------- test/int/arithmetic.cpp | 18 ++++++++++----- 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/gecode/int/arithmetic/product-mod.hpp b/gecode/int/arithmetic/product-mod.hpp index 45f9303977..5aa52c49e3 100644 --- a/gecode/int/arithmetic/product-mod.hpp +++ b/gecode/int/arithmetic/product-mod.hpp @@ -785,26 +785,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { return ES_OK; if (b.zero() && (rm == RM_IMP)) return ES_OK; - if (x.size() == 0) { - const int identity = 1 % m; - const bool t = y.assigned() ? (y.val() == identity) : false; - if (b.one() && (rm != RM_PMI)) { - GECODE_ME_CHECK(y.eq(home,identity)); return ES_OK; - } - if (b.zero() && (rm != RM_IMP)) { - GECODE_ME_CHECK(y.nq(home,identity)); return ES_OK; - } - if (!y.in(identity)) { - if (rm != RM_PMI) - GECODE_ME_CHECK(b.zero(home)); - return ES_OK; - } - if (t) { - if (rm != RM_IMP) - GECODE_ME_CHECK(b.one(home)); - return ES_OK; - } - } + if (x.size() == 0) + return Rel::ReEqDomInt::post(home,y,1 % m,b); switch (product_mod_status(x,y,m)) { case RT_TRUE: if (rm != RM_IMP) GECODE_ME_CHECK(b.one(home)); diff --git a/gecode/int/arithmetic/product.hpp b/gecode/int/arithmetic/product.hpp index c2c2edbae2..0d676409a2 100644 --- a/gecode/int/arithmetic/product.hpp +++ b/gecode/int/arithmetic/product.hpp @@ -251,6 +251,17 @@ namespace Gecode { namespace Int { namespace Arithmetic { GECODE_ME_CHECK(y.eq(home,neg ? -1 : 1)); return ES_OK; } + if (x.size() == 1) { + if (neg) + return Rel::EqBnd::post(home,x[0],MinusView(y)); + return Rel::EqBnd::post(home,x[0],y); + } + // Keep zero-aware inverse bounds when both intervals contain zero; + // binary multiplication does less pruning in that case. + if ((x.size() == 2) && !neg && + ((x[0] == x[1]) || (x[0].min() > 0) || (x[0].max() < 0) || + (x[1].min() > 0) || (x[1].max() < 0))) + return MultBnd::post(home,x[0],x[1],y); (void) new (home) Product(home,x,y,neg); return ES_OK; } @@ -538,23 +549,10 @@ namespace Gecode { namespace Int { namespace Arithmetic { ReProduct::post(Home home, ViewArray& x, IntView y, BoolView b) { if (b.one() && (rm == RM_PMI)) return ES_OK; if (b.zero() && (rm == RM_IMP)) return ES_OK; - if (x.size() == 0) { - bool t = y.assigned() ? (y.val() == 1) : false; - if (b.one() && (rm != RM_PMI)) { - GECODE_ME_CHECK(y.eq(home,1)); return ES_OK; - } - if (b.zero() && (rm != RM_IMP)) { - GECODE_ME_CHECK(y.nq(home,1)); return ES_OK; - } - if (!y.in(1)) { - if (rm != RM_PMI) GECODE_ME_CHECK(b.zero(home)); - return ES_OK; - } - if (t) { - if (rm != RM_IMP) GECODE_ME_CHECK(b.one(home)); - return ES_OK; - } - } + if (x.size() == 0) + return Rel::ReEqDomInt::post(home,y,1,b); + if (x.size() == 1) + return Rel::ReEqBnd::post(home,x[0],y,b); (void) new (home) ReProduct(home,x,y,b); return ES_OK; } diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 8d54c3b320..5306a83841 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -456,8 +456,7 @@ namespace Test { namespace Int { Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::Empty::"+str(ipl)+"::"+s, 1,d,true,ipl) { - // Reified y=1 can miss an interior removal of 1 on PC_INT_BND. - contest = CTL_NONE; testfix=false; + contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == 1; @@ -477,7 +476,7 @@ namespace Test { namespace Int { ProductSingleton(const std::string& s, const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::Product::Singleton::"+str(ipl)+"::"+s, - 2,d,true,ipl) { contest = CTL_NONE; testfix=false; } + 2,d,true,ipl) { contest = CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == x[1]; } @@ -785,8 +784,15 @@ namespace Test { namespace Int { IntVar y(home,20,24); product(home,IntVarArgs({x,q}),y); if ((home.status() == SS_FAILED) || - !bounds(x,cases[i].xmin,cases[i].xmax)) + !bounds(x,cases[i].xmin,cases[i].xmax)) { + if (opt.log) + olog << "Inverse bounds, cofactor [" << cases[i].qmin + << "," << cases[i].qmax << "]: x=" << x + << ", q=" << q << ", y=" << y << "; expected x=[" + << cases[i].xmin << "," << cases[i].xmax << "]" + << std::endl; return false; + } } { // When both the cofactor and result can be zero, no inverse @@ -862,7 +868,7 @@ namespace Test { namespace Int { ProductModEmpty(const std::string& s, const Gecode::IntSet& d, int m0, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductMod::Empty::"+str(ipl)+"::"+s, - 1,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } + 1,d,true,ipl), m(m0) { contest=CTL_NONE; } virtual bool solution(const Assignment& x) const { return x[0] == (1 % m); } @@ -1171,7 +1177,7 @@ namespace Test { namespace Int { const Gecode::IntSet& d, Gecode::IntPropLevel ipl) : Test("Arithmetic::ProductModVar::ModResultAlias::"+ - str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; testfix=false; } + str(ipl)+"::"+s,2,d,true,ipl) { contest=CTL_NONE; } virtual bool solution(const Assignment&) const { return false; } virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { Gecode::product_mod(home,Gecode::IntVarArgs({x[0]}), From 4820e52bef43ba0d5922eba94ecb0e8f252e7fc8 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 08:11:57 +0200 Subject: [PATCH 32/35] Keep fixpoint checks for ordinary GCD and modular products --- test/int/arithmetic.cpp | 44 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 5306a83841..2cab55644b 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -69,9 +69,9 @@ namespace Test { namespace Int { public: /// Create and register test GcdXYZ(const std::string& s, const Gecode::IntSet& d, - Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XYZ::"+str(ipl)+"::"+s,3,d,true,ipl) { - contest=CTL_NONE; testfix=false; + Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::Gcd::XYZ::"+str(ipl)+"::"+s+(r ? "" : "::Plain"),3,d,r,ipl) { + contest=CTL_NONE; testfix=!r; } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { @@ -93,9 +93,9 @@ namespace Test { namespace Int { public: /// Create and register test GcdXXY(const std::string& s, const Gecode::IntSet& d, - Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XXY::"+str(ipl)+"::"+s,2,d,true,ipl) { - contest=CTL_NONE; testfix=false; + Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::Gcd::XXY::"+str(ipl)+"::"+s+(r ? "" : "::Plain"),2,d,r,ipl) { + contest=CTL_NONE; testfix=!r; } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { @@ -117,9 +117,9 @@ namespace Test { namespace Int { public: /// Create and register test GcdXYX(const std::string& s, const Gecode::IntSet& d, - Gecode::IntPropLevel ipl) - : Test("Arithmetic::Gcd::XYX::"+str(ipl)+"::"+s,2,d,true,ipl) { - contest=CTL_NONE; testfix=false; + Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::Gcd::XYX::"+str(ipl)+"::"+s+(r ? "" : "::Plain"),2,d,r,ipl) { + contest=CTL_NONE; testfix=!r; } /// %Test whether \a x is solution virtual bool solution(const Assignment& x) const { @@ -843,9 +843,9 @@ namespace Test { namespace Int { int m; public: ProductModXYZR(const std::string& s, const Gecode::IntSet& d, - int m0, Gecode::IntPropLevel ipl) - : Test("Arithmetic::ProductMod::XYZR::"+str(ipl)+"::"+s, - 4,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } + int m0, Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::ProductMod::XYZR::"+str(ipl)+"::"+s+(r ? "" : "::Plain"), + 4,d,r,ipl), m(m0) { contest=CTL_NONE; testfix=!r; } virtual bool solution(const Assignment& x) const { return product_mod_value(x,3,m) == x[3]; } @@ -887,9 +887,9 @@ namespace Test { namespace Int { int m; public: ProductModSingleton(const std::string& s, const Gecode::IntSet& d, - int m0, Gecode::IntPropLevel ipl) - : Test("Arithmetic::ProductMod::Singleton::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } + int m0, Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::ProductMod::Singleton::"+str(ipl)+"::"+s+(r ? "" : "::Plain"), + 2,d,r,ipl), m(m0) { contest=CTL_NONE; testfix=!r; } virtual bool solution(const Assignment& x) const { int r = x[0] % m; if (r < 0) @@ -911,9 +911,9 @@ namespace Test { namespace Int { int m; public: ProductModXXYAlias(const std::string& s, const Gecode::IntSet& d, - int m0, Gecode::IntPropLevel ipl) - : Test("Arithmetic::ProductMod::XXYAlias::"+str(ipl)+"::"+s, - 2,d,true,ipl), m(m0) { contest=CTL_NONE; testfix=false; } + int m0, Gecode::IntPropLevel ipl, bool r=true) + : Test("Arithmetic::ProductMod::XXYAlias::"+str(ipl)+"::"+s+(r ? "" : "::Plain"), + 2,d,r,ipl), m(m0) { contest=CTL_NONE; testfix=!r; } virtual bool solution(const Assignment& x) const { long long int a = x[0] % m; long long int b = x[1] % m; @@ -2564,9 +2564,13 @@ namespace Test { namespace Int { for (IntPropLevels ipls; ipls(); ++ipls) { (void) new GcdXYZ("C",c,ipls.ipl()); + (void) new GcdXYZ("C",c,ipls.ipl(),false); (void) new GcdXYZ("Sparse",g,ipls.ipl()); + (void) new GcdXYZ("Sparse",g,ipls.ipl(),false); (void) new GcdXXY("C",c,ipls.ipl()); + (void) new GcdXXY("C",c,ipls.ipl(),false); (void) new GcdXYX("C",c,ipls.ipl()); + (void) new GcdXYX("C",c,ipls.ipl(),false); (void) new GcdXXX("C",c,ipls.ipl()); (void) new DividesXY("C",c,ipls.ipl()); @@ -2581,11 +2585,15 @@ namespace Test { namespace Int { (void) new ProductXXYAlias("C",q,ipls.ipl()); (void) new ProductModXYZR("C",q,5,ipls.ipl()); + (void) new ProductModXYZR("C",q,5,ipls.ipl(),false); (void) new ProductModXYZR("Sparse",g,7,ipls.ipl()); + (void) new ProductModXYZR("Sparse",g,7,ipls.ipl(),false); (void) new ProductModEmpty("C",q,5,ipls.ipl()); (void) new ProductModEmpty("ModulusOne",q,1,ipls.ipl()); (void) new ProductModSingleton("C",g,5,ipls.ipl()); + (void) new ProductModSingleton("C",g,5,ipls.ipl(),false); (void) new ProductModXXYAlias("C",q,5,ipls.ipl()); + (void) new ProductModXXYAlias("C",q,5,ipls.ipl(),false); (void) new ProductModVarXYMR("C",q,ipls.ipl()); (void) new ProductModVarXYMR("Sparse",g,ipls.ipl()); From 4052487ab9f829d5400d5fd7d3d9cd842ee6723b Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 08:19:11 +0200 Subject: [PATCH 33/35] Report arithmetic regression scenarios and remove obsolete cutoff tests --- test/int/arithmetic.cpp | 367 +++++++++++++++++++++++----------------- 1 file changed, 213 insertions(+), 154 deletions(-) diff --git a/test/int/arithmetic.cpp b/test/int/arithmetic.cpp index 2cab55644b..6a9bb35c78 100644 --- a/test/int/arithmetic.cpp +++ b/test/int/arithmetic.cpp @@ -210,6 +210,14 @@ namespace Test { namespace Int { } }; + /// Report a targeted arithmetic failure through the standard test log. + bool arithmetic_failed(const std::string& what, + const Gecode::VarArgArray& x) { + if (opt.log) + olog << what << ": " << x << std::endl; + return false; + } + /// Sparse endpoints must reach a fixpoint after a divisor is assigned. class NumberTheorySparseBounds : public ::Test::Base { class TestSpace : public Gecode::Space { @@ -230,10 +238,14 @@ namespace Test { namespace Int { else gcd(home,x,y,g); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("initial propagation: x, y, g", + Gecode::IntVarArgs() << x << y << g); rel(home,g,IRT_EQ,2); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val()!=6)) - return false; + return arithmetic_failed + ("assigned divisor: x, y, g", + Gecode::IntVarArgs() << x << y << g); } return true; } @@ -275,7 +287,9 @@ namespace Test { namespace Int { for (int kind=0; kind<5; kind++) { TestSpace home(kind); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("initial propagation: home.x, home.c, home.y, home.m", + Gecode::IntVarArgs() << home.x << home.c << home.y << home.m); std::unique_ptr clone (static_cast(home.clone())); PropagatorGroup::all.disable(*clone); @@ -283,7 +297,10 @@ namespace Test { namespace Int { PropagatorGroup::all.enable(*clone); if ((clone->status() == SS_FAILED) || ((kind != 4) && (!clone->x.assigned() || (clone->x.val() != 6)))) - return false; + return arithmetic_failed + ("activation after cloning, kind "+str(kind)+": x, c, y, m", + Gecode::IntVarArgs() << clone->x << clone->c + << clone->y << clone->m); // Clone the activated actor before fixing a variable modulus. std::unique_ptr child (static_cast(clone->clone())); @@ -291,7 +308,10 @@ namespace Test { namespace Int { if ((child->status() == SS_FAILED) || !child->x.assigned() || (child->x.val() != (kind == 4 ? 3 : 6)) || home.b.assigned() || (home.x.size() != 4) || clone->m.assigned()) - return false; + return arithmetic_failed + ("fixed modulus in child, kind "+str(kind)+": child x, m; parent x, m", + Gecode::IntVarArgs() << child->x << child->m + << clone->x << clone->m); } return true; } @@ -317,16 +337,31 @@ namespace Test { namespace Int { if (kind == 1) product(home,IntVarArgs({x}),x,Reify(b,rm)); if (kind == 2) product_mod(home,IntVarArgs({zero,x}),m,zero,Reify(b,rm)); if (kind == 3) product_mod(home,IntVarArgs({x}),one,zero,Reify(b,rm)); - if (home.status() == SS_FAILED) return false; + if (home.status() == SS_FAILED) + return arithmetic_failed + ("initial identity: x, zero, m, one", + Gecode::IntVarArgs() << x << zero << m << one); if (kind == 2) { // Zero residue alone does not establish a positive modulus. - if (b.assigned() || (m.min() != 0)) return false; + if (b.assigned() || (m.min() != 0)) + return arithmetic_failed + ("modulus positivity unknown: x, zero, m, one", + Gecode::IntVarArgs() << x << zero << m << one); rel(home,m,IRT_GQ,1); - if (home.status() == SS_FAILED) return false; + if (home.status() == SS_FAILED) + return arithmetic_failed + ("positive modulus: x, zero, m, one", + Gecode::IntVarArgs() << x << zero << m << one); } if ((rm == RM_IMP) ? b.assigned() : - (!b.assigned() || (b.val() != 1))) return false; - if ((x.min() != -7) || (x.max() != 7)) return false; + (!b.assigned() || (b.val() != 1))) + return arithmetic_failed + ("reified truth: x, zero, m, one", + Gecode::IntVarArgs() << x << zero << m << one); + if ((x.min() != -7) || (x.max() != 7)) + return arithmetic_failed + ("unconstrained factor: x, zero, m, one", + Gecode::IntVarArgs() << x << zero << m << one); } return true; } @@ -363,7 +398,9 @@ namespace Test { namespace Int { (g.val() != c[2]) || !b.assigned() || (b.val() != c[3]) || !correct.assigned() || (correct.val() != 1) || !wrong.assigned() || (wrong.val() != 0)) - return false; + return arithmetic_failed + ("signed GCD and divisibility: x, y, g, expected, other", + Gecode::IntVarArgs() << x << y << g << expected << other); } // hi-1 is -1 modulo hi. Its square is 1 and its cube is -1; // negating one factor reverses these residues. The cube exceeds @@ -379,11 +416,17 @@ namespace Test { namespace Int { IntVar m(home,hi-1,hi), y(home,0,hi-1); if (variable) product_mod(home,x,m,y); else product_mod(home,x,hi,y); - if (home.status() == SS_FAILED) return false; + if (home.status() == SS_FAILED) + return arithmetic_failed + ("large product before rewrite: x, m, y", + Gecode::IntVarArgs() << x << m << y); // Force variable-modulus rewriting after initial propagation. rel(home,m,IRT_EQ,hi); if ((home.status() == SS_FAILED) || !y.assigned() || - (y.val() != expected)) return false; + (y.val() != expected)) + return arithmetic_failed + ("large product residue: x, m, y", + Gecode::IntVarArgs() << x << m << y); } for (ReifyMode rm : {RM_EQV,RM_IMP,RM_PMI}) for (int truth=0; truth<=1; truth++) @@ -398,7 +441,10 @@ namespace Test { namespace Int { else product_mod(home,x,hi,y,Reify(b,rm)); const bool holds=(rm == RM_EQV) ? (control == truth) : ((rm == RM_IMP) ? (!control || truth) : (!truth || control)); - if ((home.status() != SS_FAILED) != holds) return false; + if ((home.status() != SS_FAILED) != holds) + return arithmetic_failed + ("large product reification: x, m, y", + Gecode::IntVarArgs() << x << m << y); } } return true; @@ -530,7 +576,9 @@ namespace Test { namespace Int { product(home,x,y,Reify(b)); if ((home.status() == SS_FAILED) || (y.min() != 8) || (y.max() != 1000000)) - return false; + return arithmetic_failed + ("forward bounds: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -538,10 +586,14 @@ namespace Test { namespace Int { IntVar y(home,1000,1000); product(home,x,y); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("inverse bounds failure: x, y", + Gecode::IntVarArgs() << x << y); for (int i=0; i(home.clone()); PropagatorGroup::all.disable(*clone); rel(home,home.x,IRT_NQ,0); @@ -648,7 +718,9 @@ namespace Test { namespace Int { (clone->y.min()==2); delete clone; if (!ok) - return false; + return arithmetic_failed + ("rescheduling: home.x, home.q, home.y", + Gecode::IntVarArgs() << home.x << home.q << home.y); } return true; } @@ -671,7 +743,9 @@ namespace Test { namespace Int { IntVar x(home,-10,10), y(home,-10,100); product(home,IntVarArgs({x,x}),y); if ((home.status() == SS_FAILED) || (y.min() != 0)) - return false; + return arithmetic_failed + ("square sign: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -679,7 +753,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,x}),y); if ((home.status() == SS_FAILED) || (x.min() != -5) || (x.max() != 5)) - return false; + return arithmetic_failed + ("square inverse: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -687,7 +763,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,x,x}),y); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val() != 3)) - return false; + return arithmetic_failed + ("cube inverse: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -695,7 +773,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,x,x}),y); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val() != -3)) - return false; + return arithmetic_failed + ("negative cube inverse: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -703,7 +783,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,x,two}),y); if ((home.status() == SS_FAILED) || (x.min() != -6) || (x.max() != 6)) - return false; + return arithmetic_failed + ("square cofactor: x, two, y", + Gecode::IntVarArgs() << x << two << y); } { // x*y=y has only the zero branch when x cannot be one. @@ -712,7 +794,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,y}),y); if ((home.status() == SS_FAILED) || !y.assigned() || (y.val() != 0)) - return false; + return arithmetic_failed + ("zero result alias: x, y", + Gecode::IntVarArgs() << x << y); } { // A nonzero result permits cancellation of one result occurrence. @@ -721,7 +805,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,y}),y); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val() != 1)) - return false; + return arithmetic_failed + ("nonzero cancellation: x, y", + Gecode::IntVarArgs() << x << y); } { // Cancelling one of two result occurrences leaves y*x=1. @@ -730,7 +816,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({y,y,x}),y); if ((home.status() == SS_FAILED) || !x.assigned() || !y.assigned() || (x.val() != -1) || (y.val() != -1)) - return false; + return arithmetic_failed + ("repeated result cancellation: x, y", + Gecode::IntVarArgs() << x << y); } { // Direct n-ary evaluation retains zero after an overflowing prefix. @@ -739,7 +827,9 @@ namespace Test { namespace Int { IntVar a(home,hi,hi), z(home,0,0), y(home,0,0); product(home,IntVarArgs({a,a,z}),y); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("overflow followed by zero: a, z, y", + Gecode::IntVarArgs() << a << z << y); } { TestSpace home; @@ -748,7 +838,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,x}),y,Reify(b,RM_EQV)); if ((home.status() == SS_FAILED) || (x.min() != -5) || (x.max() != 5)) - return false; + return arithmetic_failed + ("reified square: x, y", + Gecode::IntVarArgs() << x << y); } return true; } @@ -801,14 +893,18 @@ namespace Test { namespace Int { IntVar x(home,-100,100), q(home,-2,3), y(home,-10,10); product(home,IntVarArgs({x,q}),y); if ((home.status() == SS_FAILED) || !bounds(x,-100,100)) - return false; + return arithmetic_failed + ("unconstrained zero product: x, q, y", + Gecode::IntVarArgs() << x << q << y); } { TestSpace home; IntVar x(home,-100,100), zero(home,0,0), y(home,1,2); product(home,IntVarArgs({x,zero}),y); if (home.status() != SS_FAILED) - return false; + return arithmetic_failed + ("impossible zero product: x, zero, y", + Gecode::IntVarArgs() << x << zero << y); } { TestSpace home; @@ -817,7 +913,9 @@ namespace Test { namespace Int { product(home,IntVarArgs({x,a,b}),y); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val() != 0)) - return false; + return arithmetic_failed + ("overflowing cofactor: x, a, b, y", + Gecode::IntVarArgs() << x << a << b << y); } return true; } @@ -951,7 +1049,9 @@ namespace Test { namespace Int { product_mod(home,x,1,y); if ((home.status() == SS_FAILED) || !y.assigned() || (y.val() != 0)) - return false; + return arithmetic_failed + ("modulus one: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -959,7 +1059,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({z,x}),7,y); if ((home.status() == SS_FAILED) || !y.assigned() || (y.val() != 0)) - return false; + return arithmetic_failed + ("zero residue factor: z, x, y", + Gecode::IntVarArgs() << z << x << y); } { TestSpace home; @@ -967,7 +1069,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({one,x}),100,y); if ((home.status() == SS_FAILED) || (y.min() != 20) || (y.max() != 30)) - return false; + return arithmetic_failed + ("nonwrapping product: one, x, y", + Gecode::IntVarArgs() << one << x << y); } { TestSpace home; @@ -975,7 +1079,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({x}),7,y); if ((home.status() == SS_FAILED) || (y.min() != 1) || (y.max() != 5)) - return false; + return arithmetic_failed + ("positive quotient band: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -983,7 +1089,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({x}),7,y); if ((home.status() == SS_FAILED) || (y.min() != 1) || (y.max() != 5)) - return false; + return arithmetic_failed + ("negative quotient band: x, y", + Gecode::IntVarArgs() << x << y); } { TestSpace home; @@ -991,27 +1099,18 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({c,x}),15,y); if ((home.status() == SS_FAILED) || (x.min() != -96) || (x.max() != 99)) - return false; + return arithmetic_failed + ("linear congruence: c, x, y", + Gecode::IntVarArgs() << c << x << y); } { TestSpace home; IntVar c(home,6,6), x(home,-100,100), y(home,8,8); product_mod(home,IntVarArgs({c,x}),15,y); if (home.status() != SS_FAILED) - return false; - } - { - // The former Cartesian cutoff is exceeded by several orders of - // magnitude, but the zero algebra remains immediate. - TestSpace home; - IntVar zero(home,0,0); - IntVarArgs x(home,4,-100,100); - x << zero; - IntVar y(home,0,96); - product_mod(home,x,97,y); - if ((home.status() == SS_FAILED) || !y.assigned() || - (y.val() != 0)) - return false; + return arithmetic_failed + ("inconsistent congruence: c, x, y", + Gecode::IntVarArgs() << c << x << y); } { TestSpace home; @@ -1021,7 +1120,9 @@ namespace Test { namespace Int { rel(home,y,IRT_EQ,0); if ((home.status() == SS_FAILED) || !b.assigned() || (b.val() != 1)) - return false; + return arithmetic_failed + ("reified zero residue: z, x, y", + Gecode::IntVarArgs() << z << x << y); } return true; } @@ -1210,7 +1311,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs(),m,y); if ((home.status() == SS_FAILED) || (m.min() != 1) || (y.min() != 0) || (y.max() >= m.max())) - return false; + return arithmetic_failed + ("empty product range: m, y", + Gecode::IntVarArgs() << m << y); } { TestSpace home; @@ -1219,18 +1322,24 @@ namespace Test { namespace Int { IntVar y(home,0,0); product_mod(home,IntVarArgs({x}),m,y); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("integer limit modulus: x, m, y", + Gecode::IntVarArgs() << x << m << y); } { TestSpace home; IntVar x(home,3,5), c(home,2,2), m(home,5,6), y(home,2,2); product_mod(home,IntVarArgs({x,c}),m,y); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("delayed factor bounds: x, c, m, y", + Gecode::IntVarArgs() << x << c << m << y); rel(home,x,IRT_LQ,4); if ((home.status() == SS_FAILED) || !x.assigned() || (x.val() != 4)) - return false; + return arithmetic_failed + ("nonwrapping variable modulus: x, c, m, y", + Gecode::IntVarArgs() << x << c << m << y); } { TestSpace home; @@ -1239,7 +1348,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({x,c}),m,y); if ((home.status() == SS_FAILED) || (y.min() != 100) || (y.max() != 400)) - return false; + return arithmetic_failed + ("check 5: x, c, m, y", + Gecode::IntVarArgs() << x << c << m << y); } return true; } @@ -1272,7 +1383,9 @@ namespace Test { namespace Int { IntVar y(home,0,100); product_mod(home,IntVarArgs({z,m}),m,y); if ((home.status() == SS_FAILED) || !y.assigned() || (y.val()!=0)) - return false; + return arithmetic_failed + ("modulus factor: m, z, y", + Gecode::IntVarArgs() << m << z << y); } { TestSpace home; @@ -1281,7 +1394,9 @@ namespace Test { namespace Int { IntVar y(home,0,0); product_mod(home,IntVarArgs(),m,y); if ((home.status() == SS_FAILED) || !m.assigned() || (m.val()!=1)) - return false; + return arithmetic_failed + ("empty product zero: m, y", + Gecode::IntVarArgs() << m << y); } { TestSpace home; @@ -1291,7 +1406,9 @@ namespace Test { namespace Int { IntVar y(home,1,1); product_mod(home,IntVarArgs(),m,y); if ((home.status() == SS_FAILED) || !domain(m,keep,3)) - return false; + return arithmetic_failed + ("empty product one: m, y", + Gecode::IntVarArgs() << m << y); } { TestSpace home; @@ -1302,7 +1419,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({a,c}),m,y); if ((home.status() == SS_FAILED) || (m.min() != 5) || (m.max() != 56)) - return false; + return arithmetic_failed + ("divisor upper bound: a, c, m, y", + Gecode::IntVarArgs() << a << c << m << y); } { TestSpace home; @@ -1313,7 +1432,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({a,c}),m,y); if ((home.status() == SS_FAILED) || (m.min() != 4) || (m.max() != 60)) - return false; + return arithmetic_failed + ("sparse divisor range: a, c, m, y", + Gecode::IntVarArgs() << a << c << m << y); } { TestSpace home; @@ -1322,7 +1443,9 @@ namespace Test { namespace Int { IntVar m(home,IntSet(mv,3)); product_mod(home,IntVarArgs({y}),m,y); if ((home.status() == SS_FAILED) || !domain(m,mv,3)) - return false; + return arithmetic_failed + ("result factor: y, m", + Gecode::IntVarArgs() << y << m); } { TestSpace home; @@ -1331,7 +1454,9 @@ namespace Test { namespace Int { IntVar m(home,IntSet(mv,3)); product_mod(home,IntVarArgs({z}),m,y); if ((home.status() == SS_FAILED) || !domain(m,mv,3)) - return false; + return arithmetic_failed + ("zero factor: z, y, m", + Gecode::IntVarArgs() << z << y << m); } { TestSpace home; @@ -1340,7 +1465,9 @@ namespace Test { namespace Int { IntVar m(home,2,hi), y(home,0,0); product_mod(home,IntVarArgs({a,c,d}),m,y); if (home.status() == SS_FAILED) - return false; + return arithmetic_failed + ("overflowing assigned product: a, c, d, m, y", + Gecode::IntVarArgs() << a << c << d << m << y); } { TestSpace home; @@ -1349,7 +1476,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({x}),m,y); if ((home.status() == SS_FAILED) || (m.min() != 1) || (m.max() != hi)) - return false; + return arithmetic_failed + ("limit dividend: x, m, y", + Gecode::IntVarArgs() << x << m << y); } { TestSpace home; @@ -1358,7 +1487,9 @@ namespace Test { namespace Int { product_mod(home,IntVarArgs({x}),m,y); if ((home.status() == SS_FAILED) || (m.min() != 2) || (m.max() != hi-1)) - return false; + return arithmetic_failed + ("limit difference: x, m, y", + Gecode::IntVarArgs() << x << m << y); } return true; } @@ -1398,87 +1529,14 @@ namespace Test { namespace Int { if ((home.status() == SS_FAILED) || !unchanged(x,xv,5) || !unchanged(m,mv,4) || !unchanged(y,yv,4)) - return false; + return arithmetic_failed + ("inactive implication: x, m, y", + Gecode::IntVarArgs() << x << m << y); } return true; } }; - /// Staged assigned leaves above the former Cartesian support cutoff - class ArithmeticLargeLeaves : public ::Test::Base { - protected: - class TestSpace : public Gecode::Space { - public: - virtual Gecode::Space* copy(void) { return nullptr; } - }; - static bool control(const Gecode::BoolVar& b, Gecode::ReifyMode rm, - bool truth) { - if (truth && (rm != Gecode::RM_IMP)) - return b.assigned() && (b.val() == 1); - if (!truth && (rm != Gecode::RM_PMI)) - return b.assigned() && (b.val() == 0); - return true; - } - public: - ArithmeticLargeLeaves(void) - : ::Test::Base("Int::Arithmetic::LargeAssignedLeaves") {} - virtual bool run(void) { - using namespace Gecode; - const ReifyMode rms[] = {RM_EQV,RM_IMP,RM_PMI}; - for (unsigned int r=0; r<3; r++) - for (int truth=0; truth<=1; truth++) { - { - TestSpace home; - IntVar a(home,1,1000), c(home,1,1000), g(home,0,1000); - BoolVar b(home,0,1); - gcd(home,a,c,g,Reify(b,rms[r])); - rel(home,a,IRT_EQ,84); rel(home,c,IRT_EQ,30); - rel(home,g,IRT_EQ,truth ? 6 : 7); - if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) - return false; - } - { - TestSpace home; - IntVar d(home,1,1000), n(home,1,1000); BoolVar b(home,0,1); - divides(home,d,n,Reify(b,rms[r])); - rel(home,d,IRT_EQ,truth ? 7 : 8); rel(home,n,IRT_EQ,84); - if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) - return false; - } - { - TestSpace home; - IntVarArgs x(home,3,1,1000); IntVar y(home,1,1000000000); - BoolVar b(home,0,1); product(home,x,y,Reify(b,rms[r])); - rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); - rel(home,x[2],IRT_EQ,5); rel(home,y,IRT_EQ,truth ? 30 : 31); - if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) - return false; - } - { - TestSpace home; - IntVarArgs x(home,3,1,1000); IntVar y(home,0,96); - BoolVar b(home,0,1); - product_mod(home,x,97,y,Reify(b,rms[r])); - rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); - rel(home,x[2],IRT_EQ,5); rel(home,y,IRT_EQ,truth ? 30 : 31); - if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) - return false; - } - { - TestSpace home; - IntVarArgs x(home,3,1,1000); IntVar m(home,1,1000); - IntVar y(home,0,999); BoolVar b(home,0,1); - product_mod(home,x,m,y,Reify(b,rms[r])); - rel(home,x[0],IRT_EQ,2); rel(home,x[1],IRT_EQ,3); - rel(home,x[2],IRT_EQ,5); rel(home,m,IRT_EQ,97); - rel(home,y,IRT_EQ,truth ? 30 : 31); - if ((home.status() == SS_FAILED) || !control(b,rms[r],truth)) - return false; - } - } - return true; - } - }; /// %Test that a nonpositive modular-product modulus is rejected class ProductModInvalidModulus : public ::Test::Base { @@ -1496,7 +1554,9 @@ namespace Test { namespace Int { for (int m=0; m>=-1; m--) { try { Gecode::product_mod(home,Gecode::IntVarArgs(),m,y); - return false; + return arithmetic_failed + ("accepted invalid modulus: y", + Gecode::IntVarArgs() << y); } catch (const Gecode::Int::OutOfLimits&) { } } @@ -2762,7 +2822,6 @@ namespace Test { namespace Int { (void) new ProductModVarBounds; (void) new ProductModVarAlgebraic; (void) new ProductModVarInactive; - (void) new ArithmeticLargeLeaves; (void) new ProductBoundsLarge; (void) new ProductSimplifySign; (void) new ProductPowerAlias; From 6b3e1e7870d48cfa6ff42bd5d5b8e42987641d16 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 08:58:47 +0200 Subject: [PATCH 34/35] Document arithmetic actor state and lifecycle declarations --- gecode/int/arithmetic.hh | 81 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index fe4ecdc400..7638e7b949 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -794,11 +794,17 @@ namespace Gecode { namespace Int { namespace Arithmetic { virtual ExecStatus propagate(Space& home, const ModEventDelta& med); }; - /** \brief Reified bounds propagator for \f$\gcd(x_0,x_1)=x_2\f$ */ + /** \brief Reified bounds propagator for \f$\gcd(x_0,x_1)=x_2\f$ + * + * Requires \code #include \endcode + * \ingroup FuncIntProp + */ template class ReGcd : public Propagator { protected: + /// Operands and greatest common divisor IntView x0, x1, x2; + /// Reification control BoolView b; /// Constructor for posting ReGcd(Home home, IntView x0, IntView x1, IntView x2, BoolView b); @@ -864,42 +870,65 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { /** \brief Bounds propagator for an exact n-ary product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ class Product : public NaryOnePropagator { protected: using NaryOnePropagator::x; using NaryOnePropagator::y; + /// Whether the remaining product is negated bool neg; + /// Constructor for posting Product(Home home, ViewArray& x, IntView y, bool neg); + /// Constructor for cloning \a p Product(Space& home, Product& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, IntView y, bool neg=false); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size virtual size_t dispose(Space& home); }; /** \brief Reified bounds propagator for an exact n-ary product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ template class ReProduct : public Propagator { protected: + /// Factors ViewArray x; + /// Exact product IntView y; + /// Reification control BoolView b; + /// Constructor for posting ReProduct(Home home, ViewArray& x, IntView y, BoolView b); + /// Constructor for cloning \a p ReProduct(Space& home, ReProduct& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, IntView y, BoolView b); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Reschedule propagator virtual void reschedule(Space& home); + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size virtual size_t dispose(Space& home); }; @@ -910,85 +939,135 @@ namespace Gecode { namespace Int { namespace Arithmetic { namespace Gecode { namespace Int { namespace Arithmetic { /** \brief Bounds propagator for a fixed-modulus n-ary product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ class ProductMod : public NaryOnePropagator { protected: using NaryOnePropagator::x; using NaryOnePropagator::y; + /// Positive modulus int m; + /// Constructor for posting ProductMod(Home home, ViewArray& x, int m, IntView y); + /// Constructor for cloning \a p ProductMod(Space& home, ProductMod& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, int m, IntView y); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); }; /** \brief Bounds propagator for a variable-modulus n-ary product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ class ProductModVar : public Propagator { protected: + /// Factors ViewArray x; + /// Positive modulus IntView m; + /// Canonical residue IntView y; + /// Constructor for posting ProductModVar(Home home, ViewArray& x, IntView m, IntView y); + /// Constructor for cloning \a p ProductModVar(Space& home, ProductModVar& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, IntView m, IntView y); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Reschedule propagator virtual void reschedule(Space& home); + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size virtual size_t dispose(Space& home); }; /** \brief Reified bounds propagator for a variable-modulus product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ template class ReProductModVar : public Propagator { protected: + /// Factors ViewArray x; + /// Modulus (positivity is part of the reified proposition) IntView m; + /// Canonical residue IntView y; + /// Reification control BoolView b; + /// Constructor for posting ReProductModVar(Home home, ViewArray& x, IntView m, IntView y, BoolView b); + /// Constructor for cloning \a p ReProductModVar(Space& home, ReProductModVar& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, IntView m, IntView y, BoolView b); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Reschedule propagator virtual void reschedule(Space& home); + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size virtual size_t dispose(Space& home); }; /** \brief Reified bounds propagator for a fixed-modulus n-ary product + * + * Requires \code #include \endcode * \ingroup FuncIntProp */ template class ReProductMod : public Propagator { protected: + /// Factors ViewArray x; + /// Canonical residue IntView y; + /// Reification control BoolView b; + /// Positive modulus int m; + /// Constructor for posting ReProductMod(Home home, ViewArray& x, int m, IntView y, BoolView b); + /// Constructor for cloning \a p ReProductMod(Space& home, ReProductMod& p); public: + /// Post propagator static ExecStatus post(Home home, ViewArray& x, int m, IntView y, BoolView b); + /// Copy propagator during cloning virtual Actor* copy(Space& home); + /// Cost function virtual PropCost cost(const Space& home, const ModEventDelta& med) const; + /// Reschedule propagator virtual void reschedule(Space& home); + /// Perform propagation virtual ExecStatus propagate(Space& home, const ModEventDelta& med); + /// Delete propagator and return its size virtual size_t dispose(Space& home); }; From 3d0fbd09dcaf33f34e39ea4a69af12c01aafca71 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 09:00:16 +0200 Subject: [PATCH 35/35] Remove PR-only review and benchmark artifacts before merge --- docs/gcd-pr-readiness.md | 134 ----------------------------------- misc/bench-number-theory.cpp | 50 ------------- misc/bench-number-theory.py | 22 ------ 3 files changed, 206 deletions(-) delete mode 100644 docs/gcd-pr-readiness.md delete mode 100644 misc/bench-number-theory.cpp delete mode 100644 misc/bench-number-theory.py diff --git a/docs/gcd-pr-readiness.md b/docs/gcd-pr-readiness.md deleted file mode 100644 index 9ac092c070..0000000000 --- a/docs/gcd-pr-readiness.md +++ /dev/null @@ -1,134 +0,0 @@ -# Integer arithmetic readiness for Gecode 6.5.0 - -## Assessment - -The local implementation and verification work is complete. The remaining -release gate is the supported-platform CI for [draft PR #240](https://github.com/Gecode/gecode/pull/240). -This is not yet a claim that the complete release matrix is green. - -Reviewed initially at `06a6851d54`; fixes and follow-up work are integrated -with `origin/main` at `6b7de57b04` in merge commit `00d8d061ae`. -The changelog contains one unreleased 6.5.0 section. Version/SOVERSION changes -remain a release-wide responsibility. - -## Completed work - -- Fixed premature fixpoint returns in GCD and positive reified divisibility. - A delayed divisor/GCD assignment to 2 previously left `x={3,5,6}` after - propagation, although reposting reduced it to `{6}`. Domain changes now - request another propagation pass. A focused regression covers both cases. -- Removed repeated space allocation for temporary result-alias product arrays. - The existing omit-index interval helper now handles that case; allocation - remains only when a rewrite retains the new array. -- Updated Autoconf dependencies for all four headers, public API propagation - and exception documentation, and the 6.5.0 release note. -- Added activation, disable/enable rescheduling, cloning, and variable-to-fixed - modulus rewrite checks. Restored generic fixpoint comparisons for - `gcd(x,x,x)` and `divides(x,x)`. -- Added independent endpoint and modular square/cube identities, including - products beyond signed 64-bit range, delayed modulus assignment, negative - factors, and all reification truth/control combinations. -- Recognize zero products and singleton result aliases in reified exact - products, and zero-factor/modulus-one identities with variable moduli. - Positivity remains part of the proposition; the tests check that distinction. -- Audited UBSan testing exposed pre-existing overflow in the `DivMod` and - `Mod` test oracles. Their arithmetic now uses signed 64-bit intermediates. -- All propagator tests live in Gecode's testing framework in - `test/int/arithmetic.cpp`, registered like other propagator tests. - There are no family-specific CI steps, CTest entries, or additions to - `make check`; the normal test suite handles these propagators. -- Added a [modeling example](integer-number-theory.md) covering signs, - Euclidean residues, divisibility, and implication semantics. - -The Gecode skill informed the lifecycle and memory review. No new propagation -framework, persistent cache, or support enumeration was introduced. - -## Lifecycle exceptions - -`contest=CTL_NONE` remains appropriate: these propagators do not promise -bounds or domain consistency. Most generic reposting comparisons still use -`testfix=false` for a different, documented reason: - -| Family | Interior membership that can strengthen reposting without a bounds event | -| --- | --- | -| Reified GCD | Zero or the GCD computed from assigned operands | -| Reified divides | Zero in the dividend when the divisor is zero | -| Exact product | Zero in factors/result, or an assigned product in the result | -| Fixed modular product | The computed residue, including the empty-product identity | -| Variable modular product | Zero/one in the result or a derived divisor in the modulus | - -These are intentional weak-monotonicity cases, not a license to stop before -finishing a propagator's own updates. Family-specific test comments explain -them. Enabling the empty-product comparison reproduced the distinction with -seed 1576866552 and fixprob 1. `NumberTheorySparseBounds` and -`NumberTheoryLifecycle` check own-update completion and actor transitions -separately; applicable alias identity comparisons are enabled. - -## Local validation - -- All **591 integer arithmetic tests** passed in Release. -- The same **591 tests** passed in Debug with runtime auditing and UBSan, - with `UBSAN_OPTIONS=halt_on_error=1`. -- The earlier focused lifecycle selection passed three iterations with - seed 650 and fixprob 1; the completed full-suite runs used one iteration, - seed 650, and four threads. -- A CMake installation and a separate `find_package(Gecode CONFIG)` - consumer compiled, linked, and executed all nine public overloads. - Including the installed `gecode/int/arithmetic.hh` also checked the new - implementation headers. Results were GCD 6, product -216, residue 1. -- Text changelog generation, repository tidy checks, and - `git diff --check` passed. - -The local host was an Apple M1 Max with Apple Clang 21.0.0. No local Linux, -Windows, or 32-bit execution is implied by these results. Platform coverage -comes from the PR's existing CI matrix, including Autoconf and package checks. - -## Scaling sample - -This small experiment measures model construction, posting, initial -propagation, and destruction together; it is not an isolated propagation -microbenchmark. Factors have domain `0..1`, so the multiplication-chain -baseline has representable intermediates. Twenty warmup constructions precede -500 measured constructions per sample; results are medians of three samples, -in microseconds. Search is measured separately and exhaustively only at -arities 4 and 12, with the same branching order and solution count checks. - -| Arity | Factor pattern | Product (us) | Chain (us) | -| --- | --- | ---: | ---: | -| 4 | distinct | 0.446 | 0.371 | -| 4 | repeated | 0.379 | 0.435 | -| 4 | result alias | 0.475 | 0.362 | -| 12 | distinct | 0.934 | 0.888 | -| 12 | repeated | 0.662 | 0.958 | -| 12 | result alias | 1.136 | 0.838 | -| 64 | distinct | 8.496 | 3.901 | -| 64 | repeated | 6.669 | 4.291 | -| 64 | result alias | 13.094 | 3.921 | -| 256 | distinct | 93.891 | 13.239 | -| 256 | repeated | 76.150 | 14.271 | -| 256 | result alias | 166.132 | 13.393 | - -At arity 12, distinct factors took 8,189 search nodes for both implementations. -Repeated factors took 1 versus 3 nodes; result aliases took 4,097 versus 4,117. -All paired exhaustive runs returned the same solution counts. - -The quadratic scans are visible at larger arities. A future optimization -should group repeated views once per filtering pass and reuse prefix/suffix -intervals before considering persistent caches. This sample does not measure -modular-product scaling, general signed domains, or application-level speed, -and does not establish a statistically robust performance guarantee. -No optimization was made on the basis of these timings alone. - -The small [C++ driver](../misc/bench-number-theory.cpp) and -[Python runner](../misc/bench-number-theory.py) preserve the experiment. -Compile the driver against a Release Gecode build using C++17 and optimization, -then run `python3 misc/bench-number-theory.py /path/to/driver`. -The CSV's zero node/solution fields at arities above 12 mean search was skipped. - -## Scope deliberately left out - -MiniModel expressions, FlatZinc/MiniZinc exposure, and a non-reified -`divides` overload were excluded by the feature brief. They are possible -future API work, not incomplete deliverables for this PR. Likewise, broad -performance optimization is a follow-up, not a prerequisite for the documented -semantics of these additive constraints. diff --git a/misc/bench-number-theory.cpp b/misc/bench-number-theory.cpp deleted file mode 100644 index e003290389..0000000000 --- a/misc/bench-number-theory.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist -// SPDX-License-Identifier: MIT -#include -#include -#include -#include -#include -using namespace Gecode; -class Model : public Space { -public: - IntVarArray x; - Model(int n, const std::string& pattern, bool decomposition, bool search) - : x(*this,n,0,1) { - if (pattern == "repeated") - for (int i=1; i - (std::chrono::steady_clock::now()-start).count()/repeats; - unsigned long nodes=0, solutions=0; - if (n<=12) { - Model m(n,pattern,decomposition,true); - DFS engine(&m); - while (Model* s=engine.next()) { solutions++; delete s; } - nodes=engine.statistics().node; - } - std::cout << us << ',' << nodes << ',' << solutions << '\n'; -} diff --git a/misc/bench-number-theory.py b/misc/bench-number-theory.py deleted file mode 100644 index dfddc501c5..0000000000 --- a/misc/bench-number-theory.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2026 Mikael Zayenz Lagerkvist -# SPDX-License-Identifier: MIT -import sys -import statistics -import subprocess - -if len(sys.argv) != 2: - raise SystemExit("usage: bench-number-theory.py BENCHMARK_EXECUTABLE") - -print("arity,pattern,implementation,median_us,nodes,solutions") -for n in (4,12,64,256): - for pattern in ("distinct", "repeated", "alias"): - solutions = [] - for implementation in ("product", "chain"): - samples = [subprocess.check_output( - [sys.argv[1], str(n), pattern, implementation, "500"], - text=True, timeout=60).strip().split(",") for _ in range(3)] - solutions.append(samples[0][2]) - print(n, pattern, implementation, - round(statistics.median(float(s[0]) for s in samples), 3), - samples[0][1], samples[0][2], sep=",") - assert solutions[0] == solutions[1]