From f63ded70d387b1cf130fd11255a3172ff533b3bb Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Thu, 2 May 2024 13:33:23 +0200 Subject: [PATCH 1/5] Proper translation of count to minizinc (#462) * proper translation of count to minizinc * remove redundant count in comparison handling --------- Co-authored-by: Dimos Tsouros --- cpmpy/solvers/minizinc.py | 20 ++++++-------------- tests/test_solvers.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index e35133dfb..d270af14b 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -498,20 +498,6 @@ def zero_based(array): args_str = [self._convert_expression(e) for e in expr.args] return "alldifferent_except_0({})".format(args_str) - # count: we need the lhs and rhs together - if isinstance(expr, Comparison) and expr.args[0].name == 'count': - name = expr.name - lhs, rhs = expr.args - c = self._convert_expression(rhs) # count - x = [self._convert_expression(countable) for countable in lhs.args[0]] # array - y = self._convert_expression(lhs.args[1]) # value to count in array - functionmap = {'==': 'count_eq', '!=': 'count_neq', - '<=': 'count_geq', '>=': 'count_leq', - '>': 'count_lt', '<': 'count_gt'} - if name in functionmap: - name = functionmap[name] - return "{}({},{},{})".format(name, x, y, c) - args_str = [self._convert_expression(e) for e in expr.args] # standard expressions: comparison, operator, element if isinstance(expr, Comparison): @@ -608,6 +594,12 @@ def zero_based(array): elif expr.name == "abs": return "abs({})".format(args_str[0]) + elif expr.name == "count": + vars, val = expr.args + vars = self._convert_expression(vars) + val = self._convert_expression(val) + return "count({},{})".format(vars, val) + # a direct constraint, treat differently for MiniZinc, a text-based language # use the name as, unpack the arguments from the argument tuple elif isinstance(expr, DirectConstraint): diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 0297afc6c..6273b5ca0 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -669,3 +669,18 @@ def test_gurobi_element(self): s = cp.SolverLookup.get("gurobi", model) self.assertTrue(s.solve()) self.assertTrue(iv.value()[idx.value(), idx2.value()] == 8) + + + @pytest.mark.skipif(not CPM_minizinc.supported(), + reason="Minizinc not installed") + def test_count_mzn(self): + # bug #461 + from cpmpy.expressions.core import Operator + + iv = cp.intvar(0,10, shape=3) + x = cp.intvar(0,1) + y = cp.intvar(0,1) + wsum = Operator("wsum", [[1,2,3],[x,y,cp.Count(iv,3)]]) + + m = cp.Model([x + y == 2, wsum == 9]) + self.assertTrue(m.solve(solver="minizinc")) From 0ce5e5ebe7c4d668da58fc2f77d57fd4b6aa2264 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Thu, 2 May 2024 13:41:58 +0200 Subject: [PATCH 2/5] fixed translation of alldiff_except_0 (#465) --- cpmpy/solvers/minizinc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index d270af14b..776aa914c 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -496,7 +496,7 @@ def zero_based(array): if expr.name == "alldifferent_except0": args_str = [self._convert_expression(e) for e in expr.args] - return "alldifferent_except_0({})".format(args_str) + return "alldifferent_except_0([{}])".format(",".join(args_str)) args_str = [self._convert_expression(e) for e in expr.args] # standard expressions: comparison, operator, element From 2efbe75691af71de3080950baf309bea011b82dc Mon Sep 17 00:00:00 2001 From: wout4 Date: Wed, 8 May 2024 14:14:21 +0200 Subject: [PATCH 3/5] introduce argvals() helper function Use NaN for undefined values --- cpmpy/expressions/core.py | 26 ++--- cpmpy/expressions/globalconstraints.py | 152 ++++++++++++------------- cpmpy/expressions/globalfunctions.py | 25 ++-- cpmpy/expressions/utils.py | 11 +- 4 files changed, 97 insertions(+), 117 deletions(-) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index 12b3fbeb7..e416adb6d 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -72,8 +72,8 @@ import numpy as np -from .utils import is_num, is_any_list, flatlist, argval, get_bounds, is_boolexpr, is_true_cst, is_false_cst -from ..exceptions import IncompleteFunctionError, TypeError +from .utils import is_num, is_any_list, flatlist, get_bounds, is_boolexpr, is_true_cst, is_false_cst, argvals +from ..exceptions import TypeError class Expression(object): @@ -400,10 +400,7 @@ def __repr__(self): # return the value of the expression # optional, default: None def value(self): - try: - arg_vals = [argval(a) for a in self.args] - except IncompleteFunctionError: - return False + arg_vals = argvals(self.args) if any(a is None for a in arg_vals): return None if self.name == "==": return arg_vals[0] == arg_vals[1] @@ -533,14 +530,9 @@ def value(self): if self.name == "wsum": # wsum: arg0 is list of constants, no .value() use as is - arg_vals = [self.args[0], [argval(arg) for arg in self.args[1]]] + arg_vals = [self.args[0], argvals(self.args[1])] else: - try: - arg_vals = [argval(arg) for arg in self.args] - except IncompleteFunctionError as e: - if self.is_bool(): return False - raise e - + arg_vals = argvals(self.args) if any(a is None for a in arg_vals): return None # non-boolean @@ -555,10 +547,12 @@ def value(self): try: return arg_vals[0] // arg_vals[1] except ZeroDivisionError: - raise IncompleteFunctionError(f"Division by zero during value computation for expression {self}") + return np.nan # we use NaN to represent undefined values + + # boolean context, replace NaN with False + arg_vals = [False if np.isnan(x) else x for x in arg_vals] - # boolean - elif self.name == "and": return all(arg_vals) + if self.name == "and": return all(arg_vals) elif self.name == "or" : return any(arg_vals) elif self.name == "->": return (not arg_vals[0]) or arg_vals[1] elif self.name == "not": return not arg_vals[0] diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index ea9cfddb2..823513550 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- ## ## globalconstraints.py ## @@ -108,13 +108,14 @@ def my_circuit_decomp(self): """ import copy -import warnings # for deprecation warning +import warnings # for deprecation warning import numpy as np -from ..exceptions import CPMpyException, IncompleteFunctionError, TypeError +from ..exceptions import CPMpyException, TypeError from .core import Expression, Operator, Comparison from .variables import boolvar, intvar, cpm_array, _NumVarImpl, _IntVarImpl -from .utils import flatlist, all_pairs, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds -from .globalfunctions import * # XXX make this file backwards compatible +from .utils import flatlist, all_pairs, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds, \ + is_true_cst, argvals +from .globalfunctions import * # XXX make this file backwards compatible # Base class GlobalConstraint @@ -155,13 +156,15 @@ def get_bounds(self): # Global Constraints (with Boolean return type) def alldifferent(args): - warnings.warn("Deprecated, use AllDifferent(v1,v2,...,vn) instead, will be removed in stable version", DeprecationWarning) - return AllDifferent(*args) # unfold list as individual arguments + warnings.warn("Deprecated, use AllDifferent(v1,v2,...,vn) instead, will be removed in stable version", + DeprecationWarning) + return AllDifferent(*args) # unfold list as individual arguments class AllDifferent(GlobalConstraint): """All arguments have a different (distinct) value """ + def __init__(self, *args): super().__init__("alldifferent", flatlist(args)) @@ -171,17 +174,18 @@ def decompose(self): return [var1 != var2 for var1, var2 in all_pairs(self.args)], [] def value(self): - try: - values = [argval(a) for a in self.args] - return len(set(values)) == len(self.args) - except IncompleteFunctionError: + values = argvals(self.args) + if any([np.isnan(x) for x in values]): return False + else: + return len(set(values)) == len(self.args) class AllDifferentExcept0(GlobalConstraint): """ All nonzero arguments have a distinct value """ + def __init__(self, *args): super().__init__("alldifferent_except0", flatlist(args)) @@ -190,20 +194,23 @@ def decompose(self): return [(var1 == var2).implies(var1 == 0) for var1, var2 in all_pairs(self.args)], [] def value(self): - try: - vals = [a.value() for a in self.args if a.value() != 0] - return len(set(vals)) == len(vals) - except IncompleteFunctionError: + vals = [a.value() for a in self.args if a.value() != 0] + if any([np.isnan(x) for x in vals]): return False + else: + return len(set(vals)) == len(vals) + def allequal(args): - warnings.warn("Deprecated, use AllEqual(v1,v2,...,vn) instead, will be removed in stable version", DeprecationWarning) - return AllEqual(*args) # unfold list as individual arguments + warnings.warn("Deprecated, use AllEqual(v1,v2,...,vn) instead, will be removed in stable version", + DeprecationWarning) + return AllEqual(*args) # unfold list as individual arguments class AllEqual(GlobalConstraint): """All arguments have the same value """ + def __init__(self, *args): super().__init__("allequal", flatlist(args)) @@ -214,20 +221,22 @@ def decompose(self): return [var1 == var2 for var1, var2 in zip(self.args[:-1], self.args[1:])], [] def value(self): - try: - values = [argval(a) for a in self.args] - return len(set(values)) == 1 - except IncompleteFunctionError: + values = argvals(self.args) + if any([np.isnan(x) for x in values]): return False + return len(set(values)) == 1 + def circuit(args): - warnings.warn("Deprecated, use Circuit(v1,v2,...,vn) instead, will be removed in stable version", DeprecationWarning) - return Circuit(*args) # unfold list as individual arguments + warnings.warn("Deprecated, use Circuit(v1,v2,...,vn) instead, will be removed in stable version", + DeprecationWarning) + return Circuit(*args) # unfold list as individual arguments class Circuit(GlobalConstraint): """The sequence of variables form a circuit, where x[i] = j means that j is the successor of i. """ + def __init__(self, *args): flatargs = flatlist(args) if any(is_boolexpr(arg) for arg in flatargs): @@ -246,14 +255,15 @@ def decompose(self): """ succ = cpm_array(self.args) n = len(succ) - order = intvar(0,n-1, shape=n) + order = intvar(0, n - 1, shape=n) constraining = [] - constraining += [AllDifferent(succ)] # different successors - constraining += [AllDifferent(order)] # different orders - constraining += [order[n-1] == 0] # symmetry breaking, last one is '0' + constraining += [AllDifferent(succ)] # different successors + constraining += [AllDifferent(order)] # different orders + constraining += [order[n - 1] == 0] # symmetry breaking, last one is '0' defining = [order[0] == succ[0]] - defining += [order[i] == succ[order[i-1]] for i in range(1,n)] # first one is successor of '0', ith one is successor of i-1 + defining += [order[i] == succ[order[i - 1]] for i in + range(1, n)] # first one is successor of '0', ith one is successor of i-1 return constraining, defining @@ -261,11 +271,7 @@ def value(self): pathlen = 0 idx = 0 visited = set() - try: - arr = [argval(a) for a in self.args] - except IncompleteFunctionError: - return False - + arr = argvals(self.args) while idx not in visited: if idx is None: return False @@ -274,7 +280,6 @@ def value(self): visited.add(idx) pathlen += 1 idx = arr[idx] - return pathlen == len(self.args) and idx == 0 @@ -286,8 +291,9 @@ class Inverse(GlobalConstraint): fwd[i] == x <==> rev[x] == i """ + def __init__(self, fwd, rev): - flatargs = flatlist([fwd,rev]) + flatargs = flatlist([fwd, rev]) if any(is_boolexpr(arg) for arg in flatargs): raise TypeError("Only integer arguments allowed for global constraint Inverse: {}".format(flatargs)) assert len(fwd) == len(rev) @@ -300,21 +306,19 @@ def decompose(self): return [all(rev[x] == i for i, x in enumerate(fwd))], [] def value(self): - try: - fwd = [argval(a) for a in self.args[0]] - rev = [argval(a) for a in self.args[1]] - except IncompleteFunctionError: - return False + fwd = argvals(self.args[0]) + rev = argvals(self.args[1]) # args are fine, now evaluate actual inverse cons try: return all(rev[x] == i for i, x in enumerate(fwd)) - except IndexError: # partiality of Element constraint - raise IncompleteFunctionError + except IndexError: # if index is out of range then it's definitely not Inverse. + return False class Table(GlobalConstraint): """The values of the variables in 'array' correspond to a row in 'table' """ + def __init__(self, array, table): array = flatlist(array) if not all(isinstance(x, Expression) for x in array): @@ -328,12 +332,8 @@ def decompose(self): def value(self): arr, tab = self.args - try: - arrval = [argval(a) for a in arr] - return arrval in tab - except IncompleteFunctionError: - return False - + arrval = [argval(a) for a in arr if not np.isnan(argval(a))] + return arrval in tab # syntax of the form 'if b then x == 9 else x == 0' is not supported (no override possible) @@ -347,13 +347,10 @@ def __init__(self, condition, if_true, if_false): def value(self): condition, if_true, if_false = self.args - try: - if argval(condition): - return argval(if_true) - else: - return argval(if_false) - except IncompleteFunctionError: - return False + if is_true_cst((argval(condition))): + return argval(if_true) + else: + return argval(if_false) def decompose(self): condition, if_true, if_false = self.args @@ -364,15 +361,14 @@ def __repr__(self): return "If {} Then {} Else {}".format(condition, if_true, if_false) - class InDomain(GlobalConstraint): """ The "InDomain" constraint, defining non-interval domains for an expression """ def __init__(self, expr, arr): - assert not (is_boolexpr(expr) or any(is_boolexpr(a) for a in arr)), \ - "The expressions in the InDomain constraint should not be boolean" + # assert not (is_boolexpr(expr) or any(is_boolexpr(a) for a in arr)), \ + # "The expressions in the InDomain constraint should not be boolean" super().__init__("InDomain", [expr, arr]) def decompose(self): @@ -387,8 +383,8 @@ def decompose(self): lb, ub = expr.get_bounds() defining = [] - #if expr is not a var - if not isinstance(expr,_IntVarImpl): + # if expr is not a var + if not isinstance(expr, _IntVarImpl): aux = intvar(lb, ub) defining.append(aux == expr) expr = aux @@ -399,12 +395,9 @@ def decompose(self): else: return [expr != val for val in range(lb, ub + 1) if val not in arr], defining - def value(self): - try: - return argval(self.args[0]) in argval(self.args[1]) - except IncompleteFunctionError: - return False + val = argval(self.args[0]) + return val in argval(self.args[1]) and not np.isnan(val) def __repr__(self): return "{} in {}".format(self.args[0], self.args[1]) @@ -430,14 +423,11 @@ def decompose(self): # there are multiple decompositions possible, Recursively using sum allows it to be efficient for all solvers. decomp = [sum(self.args[:2]) == 1] if len(self.args) > 2: - decomp = Xor([decomp,self.args[2:]]).decompose()[0] + decomp = Xor([decomp, self.args[2:]]).decompose()[0] return decomp, [] def value(self): - try: - return sum(argval(a) for a in self.args) % 2 == 1 - except IncompleteFunctionError: - return False + return sum(argvals(self.args)) % 2 == 1 def __repr__(self): if len(self.args) == 2: @@ -451,6 +441,7 @@ class Cumulative(GlobalConstraint): Ensures no overlap between tasks and never exceeding the capacity of the resource Supports both varying demand across tasks or equal demand for all jobs """ + def __init__(self, start, duration, end, demand, capacity): assert is_any_list(start), "start should be a list" assert is_any_list(duration), "duration should be a list" @@ -469,7 +460,7 @@ def __init__(self, start, duration, end, demand, capacity): if is_any_list(demand): demand = flatlist(demand) assert len(demand) == n_jobs, "Demand should be supplied for each task or be single constant" - else: # constant demand + else: # constant demand demand = [demand] * n_jobs super(Cumulative, self).__init__("cumulative", [start, duration, end, demand, capacity]) @@ -493,7 +484,7 @@ def decompose(self): # demand doesn't exceed capacity lb, ub = min(s.lb for s in start), max(s.ub for s in end) - for t in range(lb,ub+1): + for t in range(lb, ub + 1): demand_at_t = 0 for job in range(len(start)): if is_num(demand): @@ -506,24 +497,21 @@ def decompose(self): return cons, [] def value(self): - try: - argvals = [np.array([argval(a) for a in arg]) if is_any_list(arg) - else argval(arg) for arg in self.args] - except IncompleteFunctionError: - return False + vals = [np.array(argvals(arg)) if is_any_list(arg) + else argval(arg) for arg in self.args] - if any(a is None for a in argvals): + if any(a is None for a in vals): return None # start, dur, end are np arrays - start, dur, end, demand, capacity = argvals + start, dur, end, demand, capacity = vals # start and end seperated by duration if not (start + dur == end).all(): return False # demand doesn't exceed capacity lb, ub = min(start), max(end) - for t in range(lb, ub+1): + for t in range(lb, ub + 1): if capacity < sum(demand * ((start <= t) & (t < end))): return False @@ -540,7 +528,7 @@ def __init__(self, vars, vals, occ): flatargs = flatlist([vars, vals, occ]) if any(is_boolexpr(arg) for arg in flatargs): raise TypeError("Only numerical arguments allowed for gcc global constraint: {}".format(flatargs)) - super().__init__("gcc", [vars,vals,occ]) + super().__init__("gcc", [vars, vals, occ]) def decompose(self): from .globalfunctions import Count @@ -566,6 +554,7 @@ class DirectConstraint(Expression): If you want/need to use what the solver returns (e.g. an identifier for use in other constraints), then use `directvar()` instead, or access the solver object from the solver interface directly. """ + def __init__(self, name, arguments, novar=None): """ name: name of the solver function that you wish to call @@ -603,4 +592,3 @@ def callSolver(self, CPMpy_solver, Native_solver): solver_args[i] = CPMpy_solver.solver_vars(solver_args[i]) # len(native_args) should match nr of arguments of `native_function` return solver_function(*solver_args) - diff --git a/cpmpy/expressions/globalfunctions.py b/cpmpy/expressions/globalfunctions.py index 8c1eeecca..ff4656c21 100644 --- a/cpmpy/expressions/globalfunctions.py +++ b/cpmpy/expressions/globalfunctions.py @@ -64,10 +64,10 @@ def decompose_comparison(self): import copy import warnings # for deprecation warning import numpy as np -from ..exceptions import CPMpyException, IncompleteFunctionError, TypeError +from ..exceptions import CPMpyException, TypeError from .core import Expression, Operator, Comparison from .variables import boolvar, intvar, cpm_array, _NumVarImpl -from .utils import flatlist, all_pairs, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds +from .utils import flatlist, argval, argvals, eval_comparison, is_any_list, is_boolexpr, get_bounds class GlobalFunction(Expression): @@ -116,11 +116,12 @@ def __init__(self, arg_list): super().__init__("min", flatlist(arg_list)) def value(self): - argvals = [argval(a) for a in self.args] - if any(val is None for val in argvals): + vals = argvals(self.args) + if any(val is None for val in vals): return None else: - return min(argvals) + return min(vals) # will always return nan if any argument is nan. + # This is in line with how solvers handle undefinedness in Min and Max. def decompose_comparison(self, cpm_op, cpm_rhs): """ @@ -157,11 +158,11 @@ def __init__(self, arg_list): super().__init__("max", flatlist(arg_list)) def value(self): - argvals = [argval(a) for a in self.args] - if any(val is None for val in argvals): + vals = argvals(self.args) + if any(val is None for val in vals): return None else: - return max(argvals) + return max(vals) def decompose_comparison(self, cpm_op, cpm_rhs): """ @@ -257,8 +258,8 @@ def value(self): if idxval is not None: if idxval >= 0 and idxval < len(arr): return argval(arr[idxval]) - raise IncompleteFunctionError(f"Index {idxval} out of range for array of length {len(arr)} while calculating value for expression {self}") - return None # default + return np.nan + return None # default def decompose_comparison(self, cpm_op, cpm_rhs): """ @@ -271,8 +272,6 @@ def decompose_comparison(self, cpm_op, cpm_rhs): they should be enforced toplevel. """ - from .python_builtins import any - arr, idx = self.args return [(idx == i).implies(eval_comparison(cpm_op, arr[i], cpm_rhs)) for i in range(len(arr))] + \ [idx >= 0, idx < len(arr)], [] @@ -356,7 +355,7 @@ def decompose_comparison(self, cmp_op, cpm_rhs): return [eval_comparison(cmp_op, sum(bvars), cpm_rhs)], constraints def value(self): - return len(set(argval(a) for a in self.args)) + return len(set(argval(a) for a in self.args if not np.isnan(argval(a)))) def get_bounds(self): """ diff --git a/cpmpy/expressions/utils.py b/cpmpy/expressions/utils.py index a42e88c82..88442d290 100644 --- a/cpmpy/expressions/utils.py +++ b/cpmpy/expressions/utils.py @@ -118,14 +118,13 @@ def all_pairs(args): def argval(a): """ returns .value() of Expression, otherwise the variable itself - We check with hasattr instead of isinstance to avoid circular dependency """ - try: - return a.value() if hasattr(a, "value") else a - except IncompleteFunctionError as e: - if a.is_bool(): return False - raise e + return a.value() if hasattr(a, "value") else a + + +def argvals(arr): + return [argval(a) for a in arr] def eval_comparison(str_op, lhs, rhs): From 3f8ef4e3697b138e704c8cea9aa0aa0dbbf43ebe Mon Sep 17 00:00:00 2001 From: wout4 Date: Wed, 8 May 2024 14:26:07 +0200 Subject: [PATCH 4/5] exclude inverse from tests --- tests/test_constraints.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 046db1e14..b04208b1e 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -16,13 +16,13 @@ # Exclude some global constraints for solvers # Can be used when .value() method is not implemented/contains bugs -EXCLUDE_GLOBAL = {"ortools": {}, - "gurobi": {}, - "minizinc": {"circuit"}, - "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative"}, - "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative",'xor'}, - "exact": {}, - "choco": {} +EXCLUDE_GLOBAL = {"ortools": {'inverse'}, + "gurobi": {'inverse'}, + "minizinc": {"circuit", 'inverse'}, + "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative", 'inverse'}, + "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative",'xor', 'inverse'}, + "exact": {'inverse'}, + "choco": {'inverse'} } # Exclude certain operators for solvers. From daf505d5e1a2614d99ebbed82a4e5d8dcc629fc0 Mon Sep 17 00:00:00 2001 From: wout4 Date: Wed, 8 May 2024 16:17:27 +0200 Subject: [PATCH 5/5] include inverse again, test are failing anyway --- tests/test_constraints.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index b04208b1e..046db1e14 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -16,13 +16,13 @@ # Exclude some global constraints for solvers # Can be used when .value() method is not implemented/contains bugs -EXCLUDE_GLOBAL = {"ortools": {'inverse'}, - "gurobi": {'inverse'}, - "minizinc": {"circuit", 'inverse'}, - "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative", 'inverse'}, - "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative",'xor', 'inverse'}, - "exact": {'inverse'}, - "choco": {'inverse'} +EXCLUDE_GLOBAL = {"ortools": {}, + "gurobi": {}, + "minizinc": {"circuit"}, + "pysat": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative"}, + "pysdd": {"circuit", "element","min","max","count", "nvalue", "allequal","alldifferent","cumulative",'xor'}, + "exact": {}, + "choco": {} } # Exclude certain operators for solvers.