Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d42780f
Add missing case for Xor in normalize
ThomSerg Jul 30, 2025
05b007e
Fix Xor boolean simpify and add tests
ThomSerg Jul 30, 2025
09a006c
Remove leftover comment
ThomSerg Jul 30, 2025
87ef959
push failing tests
IgnaceBleukx Jul 31, 2025
3565761
cleanup tests
IgnaceBleukx Jul 31, 2025
67be932
More explicit arg to simplify_boolean
ThomSerg Aug 1, 2025
a336aee
Merge two cases
ThomSerg Aug 1, 2025
c584723
add special case for xor in push_down_negation
IgnaceBleukx Aug 1, 2025
91ba1a1
fix normalization for xor
IgnaceBleukx Aug 1, 2025
2c35127
minor docs
IgnaceBleukx Aug 4, 2025
ced35f8
add special case for ite
IgnaceBleukx Aug 4, 2025
ec77419
add tests
IgnaceBleukx Aug 4, 2025
db286af
refactor xor case
IgnaceBleukx Aug 4, 2025
16d3cd5
undo changes in negation.py
IgnaceBleukx Aug 8, 2025
9297713
add negation function to global constraints
IgnaceBleukx Aug 8, 2025
02cf418
use negation function
IgnaceBleukx Aug 8, 2025
c00bc58
implement custom negation for globals
IgnaceBleukx Aug 8, 2025
7d946aa
remove unused imports
IgnaceBleukx Aug 8, 2025
40bcc99
remove normalization from transformation
IgnaceBleukx Aug 8, 2025
bae7909
support for xor with constants in ortools
IgnaceBleukx Aug 8, 2025
50cc4d5
special case for xor with 1 arg in z3
IgnaceBleukx Aug 8, 2025
f7ecd90
add special cases to test_constraints.py
IgnaceBleukx Aug 8, 2025
5c0f903
Merge remote-tracking branch 'origin/master' into missing_xor_normalize
IgnaceBleukx Aug 8, 2025
8550ae3
update doc
IgnaceBleukx Aug 8, 2025
b31383c
fix negation of xor
IgnaceBleukx Aug 8, 2025
48f786e
remove impossible test
IgnaceBleukx Aug 8, 2025
0d56308
add special case for sum of 1 Boolean in linearize
IgnaceBleukx Aug 8, 2025
14bdf52
remove old tests
IgnaceBleukx Aug 8, 2025
3371cf0
indent
IgnaceBleukx Aug 8, 2025
1a090d9
Merge remote-tracking branch 'origin/master' into missing_xor_normalize
IgnaceBleukx Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion cpmpy/expressions/globalconstraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def my_circuit_decomp(self):

from .core import BoolVal
from .utils import all_pairs, is_int, is_bool, STAR
from .variables import _IntVarImpl
from .variables import _IntVarImpl, _BoolVarImpl
from .globalfunctions import * # XXX make this file backwards compatible


Expand Down Expand Up @@ -173,6 +173,14 @@ def get_bounds(self):
"""
return 0, 1

def negate(self):
"""
Returns the negation of this global constraint.
Defaults to ~self, but subclasses can implement a better version,
> Fages, François, and Sylvain Soliman. Reifying global constraints. Diss. INRIA, 2012.
"""
return ~self


# Global Constraints (with Boolean return type)
def alldifferent(args):
Expand Down Expand Up @@ -430,6 +438,9 @@ def value(self):
arrval = argvals(arr)
return arrval in tab

def negate(self):
return NegativeTable(self.args[0], self.args[1])

class ShortTable(GlobalConstraint):
"""
Extension of the `Table` constraint where the `table` matrix may contain wildcards (STAR), meaning there are
Expand Down Expand Up @@ -479,6 +490,9 @@ def value(self):
arrval = argvals(arr)
tabval = argvals(tab)
return arrval not in tabval

def negate(self):
return Table(self.args[0], self.args[1])


class Regular(GlobalConstraint):
Expand Down Expand Up @@ -596,6 +610,9 @@ def __repr__(self):
condition, if_true, if_false = self.args
return "If {} Then {} Else {}".format(condition, if_true, if_false)

def negate(self):
return IfThenElse(self.args[0], self.args[2], self.args[1])



class InDomain(GlobalConstraint):
Expand Down Expand Up @@ -636,6 +653,11 @@ def value(self):
def __repr__(self):
return "{} in {}".format(self.args[0], self.args[1])

def negate(self):
lb, ub = get_bounds(self.args[0])
return InDomain(self.args[0],
[v for v in range(lb,ub+1) if v not in set(self.args[1])])


class Xor(GlobalConstraint):
"""
Expand Down Expand Up @@ -668,6 +690,21 @@ def __repr__(self):
return "{} xor {}".format(*self.args)
return "xor({})".format(self.args)

def negate(self):
# negate one of the arguments, ideally a variable
new_args = None
for i, a in enumerate(self.args):
if isinstance(a, _BoolVarImpl):
new_args = self.args[:i] + [~a] + self.args[i+1:]
break

if new_args is None:# did not find a Boolean variable to negate
# pick first arg, and push down negation
new_args = list(self.args)
new_args[0] = cp.transformations.negation.recurse_negation(self.args[0])

return Xor(new_args)


class Cumulative(GlobalConstraint):
"""
Expand Down
4 changes: 2 additions & 2 deletions cpmpy/transformations/negation.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ def recurse_negation(expr):
# global constraints
elif hasattr(expr, "decompose"):
newexpr = copy.copy(expr)
# args are positive as we will negate the global, still check if no 'not' in its arguments
# args are positive as we will negate the global, still check if no 'not' in its arguments
newexpr.update_args(push_down_negation(expr.args, toplevel=False))
return ~newexpr
return newexpr.negate()

elif is_bool(expr): # unlikely case with non-CPMpy True or False
return ~BoolVal(expr)
Expand Down
3 changes: 2 additions & 1 deletion cpmpy/transformations/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ def simplify_boolean(lst_of_expr, num_context=False):
if is_bool(res): # Result is a Boolean constant
newlist.append(int(res) if num_context else BoolVal(res))
else: # Result is an expression
newlist.append(res)
newlist.append(res)

elif isinstance(expr, (GlobalConstraint, GlobalFunction)):
newargs = simplify_boolean(expr.args) # TODO: how to determine which are Bool/int?
if any(a1 is not a2 for a1,a2 in zip(expr.args, newargs)):
Expand Down
Loading