From c523d6ac023a096538d8000b2d0c3c26cc82ec3d Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Wed, 28 Aug 2024 18:52:04 +0200 Subject: [PATCH 01/12] cover cp.abs() using the builtin --- cpmpy/expressions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index e86de9032..e96c2a8f1 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -29,3 +29,4 @@ from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept, Among from .core import BoolVal from .python_builtins import all, any, max, min, sum +from builtins import abs From 191f8c138ff9d9d129383cbae0c681ce060b6c29 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 13:33:27 +0200 Subject: [PATCH 02/12] abs implementation --- cpmpy/expressions/__init__.py | 3 +-- cpmpy/expressions/python_builtins.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index e96c2a8f1..b695ff926 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -28,5 +28,4 @@ from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept, Among from .core import BoolVal -from .python_builtins import all, any, max, min, sum -from builtins import abs +from .python_builtins import all, any, max, min, sum, abs diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index 7fcaa805c..53aa9befd 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -17,13 +17,14 @@ max min sum + abs """ import builtins # to use the original Python-builtins from .utils import is_false_cst, is_true_cst from .variables import NDVarArray from .core import Expression, Operator -from .globalfunctions import Minimum, Maximum +from .globalfunctions import Minimum, Maximum, Abs # Overwriting all/any python built-ins @@ -125,3 +126,20 @@ def sum(*iterable, **kwargs): assert len(kwargs)==0, "sum over decision variables does not support keyword arguments" return Operator("sum", iterable) + + +def abs(*iterable, **kwargs): + """ + abs() overwrites the python built-in to support decision variables. + + if iterable does not contain CPMpy expressions, the built-in is called + else an Absolute functional global constraint is constructed; no keyword + arguments are supported in that case + """ + if len(iterable) == 1: + iterable = tuple(iterable[0]) + if not builtins.any(isinstance(elem, Expression) for elem in iterable): + return builtins.abs(*iterable, **kwargs) + + assert len(kwargs)==0, "abs over decision variables does not support keyword arguments" + return Abs(iterable) From 720c27cec7a7b3a8393f9b49ab7b9a070a0d3ec2 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 14:04:04 +0200 Subject: [PATCH 03/12] actually abs for only 1 element --- cpmpy/expressions/python_builtins.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index 53aa9befd..7f563f0df 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -21,10 +21,11 @@ """ import builtins # to use the original Python-builtins -from .utils import is_false_cst, is_true_cst +from .utils import is_false_cst, is_true_cst, is_any_list from .variables import NDVarArray from .core import Expression, Operator from .globalfunctions import Minimum, Maximum, Abs +from ..exceptions import CPMpyException # Overwriting all/any python built-ins @@ -128,18 +129,16 @@ def sum(*iterable, **kwargs): return Operator("sum", iterable) -def abs(*iterable, **kwargs): +def abs(element): """ abs() overwrites the python built-in to support decision variables. - if iterable does not contain CPMpy expressions, the built-in is called - else an Absolute functional global constraint is constructed; no keyword - arguments are supported in that case + if the element given is not a CPMpy expression, the built-in is called + else an Absolute functional global constraint is constructed. """ - if len(iterable) == 1: - iterable = tuple(iterable[0]) - if not builtins.any(isinstance(elem, Expression) for elem in iterable): - return builtins.abs(*iterable, **kwargs) + if is_any_list(element): + raise CPMpyException('abs does not accept iterables') + if not isinstance(element, Expression): + return builtins.abs(element) - assert len(kwargs)==0, "abs over decision variables does not support keyword arguments" - return Abs(iterable) + return Abs(element) From aa433187a822abe9b24fd55c6d3d5b0cc68bbbf0 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 14:04:17 +0200 Subject: [PATCH 04/12] tests for builtins --- tests/test_builtins.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_builtins.py diff --git a/tests/test_builtins.py b/tests/test_builtins.py new file mode 100644 index 000000000..569aa3dba --- /dev/null +++ b/tests/test_builtins.py @@ -0,0 +1,40 @@ +import unittest + +import cpmpy as cp +from cpmpy.exceptions import CPMpyException + +iv = cp.intvar(-8, 8, shape=5) + + +class TestBuiltin(unittest.TestCase): + + def test_max(self): + constraints = [cp.max(iv) + 9 <= 8] + model = cp.Model(constraints) + self.assertTrue(model.solve()) + self.assertTrue(cp.max(iv.value()) <= -1) + + model = cp.Model(cp.max(iv).decompose_comparison('!=', 4)) + self.assertTrue(model.solve()) + self.assertNotEqual(str(cp.max(iv.value())), '4') + + def test_min(self): + constraints = [cp.min(iv) + 9 == 8] + model = cp.Model(constraints) + self.assertTrue(model.solve()) + self.assertEqual(str(cp.min(iv.value())), '-1') + + model = cp.Model(cp.min(iv).decompose_comparison('==', 4)) + self.assertTrue(model.solve()) + self.assertEqual(str(cp.min(iv.value())), '4') + + def test_abs(self): + constraints = [cp.abs(iv[0]) + 9 <= 8] + model = cp.Model(constraints) + self.assertFalse(model.solve()) + + model = cp.Model(cp.abs(iv[0]).decompose_comparison('!=', 4)) + self.assertTrue(model.solve()) + self.assertNotEqual(str(cp.abs(iv[0].value())), '4') + + self.assertRaises(CPMpyException) From fd045fd0ba3ea98fb5b09203dbc1c8e438a2dc21 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 15:45:54 +0200 Subject: [PATCH 05/12] support vectorized --- cpmpy/expressions/python_builtins.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index 7f563f0df..76b24d7a7 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -136,9 +136,7 @@ def abs(element): if the element given is not a CPMpy expression, the built-in is called else an Absolute functional global constraint is constructed. """ - if is_any_list(element): - raise CPMpyException('abs does not accept iterables') - if not isinstance(element, Expression): + if is_any_list(element) or not isinstance(element, Expression): return builtins.abs(element) return Abs(element) From 8d42a1e3e6b3ddf234f5be58e9f1d5a0ed753e9e Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 15:50:45 +0200 Subject: [PATCH 06/12] recursive for iterable --- cpmpy/expressions/python_builtins.py | 7 +++++-- tests/test_builtins.py | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index 76b24d7a7..a7c524932 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -22,7 +22,7 @@ import builtins # to use the original Python-builtins from .utils import is_false_cst, is_true_cst, is_any_list -from .variables import NDVarArray +from .variables import NDVarArray, cpm_array from .core import Expression, Operator from .globalfunctions import Minimum, Maximum, Abs from ..exceptions import CPMpyException @@ -136,7 +136,10 @@ def abs(element): if the element given is not a CPMpy expression, the built-in is called else an Absolute functional global constraint is constructed. """ - if is_any_list(element) or not isinstance(element, Expression): + if is_any_list(element): + return cpm_array([abs(elem) for elem in element]) + + if isinstance(element, Expression): return builtins.abs(element) return Abs(element) diff --git a/tests/test_builtins.py b/tests/test_builtins.py index 569aa3dba..84a00d4b6 100644 --- a/tests/test_builtins.py +++ b/tests/test_builtins.py @@ -35,6 +35,4 @@ def test_abs(self): model = cp.Model(cp.abs(iv[0]).decompose_comparison('!=', 4)) self.assertTrue(model.solve()) - self.assertNotEqual(str(cp.abs(iv[0].value())), '4') - - self.assertRaises(CPMpyException) + self.assertNotEqual(str(cp.abs(iv[0].value())), '4') \ No newline at end of file From 55c1b43a02f5d852a00c3852c69977cdc3d6fb79 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Mon, 9 Sep 2024 16:23:26 +0200 Subject: [PATCH 07/12] fix error in test for all_diff_except_n --- tests/test_globalconstraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_globalconstraints.py b/tests/test_globalconstraints.py index 6dc630125..af822390e 100644 --- a/tests/test_globalconstraints.py +++ b/tests/test_globalconstraints.py @@ -116,7 +116,7 @@ def test_alldifferent_except_n(self): # and some more iv = cp.intvar(-8, 8, shape=3) - self.assertTrue(cp.Model([cp.AllDifferentExceptN(iv,2)]).solve()) + self.assertTrue(cp.Model([cp.AllDifferentExceptN(iv,4)]).solve()) self.assertTrue(cp.AllDifferentExceptN(iv,4).value()) self.assertTrue(cp.Model([cp.AllDifferentExceptN(iv,7), iv == [7, 7, 1]]).solve()) self.assertTrue(cp.AllDifferentExceptN(iv,7).value()) From 39be5b069f7b39ef62558d8784070db9a8523a35 Mon Sep 17 00:00:00 2001 From: wout4 Date: Thu, 19 Sep 2024 09:30:30 +0200 Subject: [PATCH 08/12] add testcase --- tests/test_builtins.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_builtins.py b/tests/test_builtins.py index 84a00d4b6..c49cb298f 100644 --- a/tests/test_builtins.py +++ b/tests/test_builtins.py @@ -33,6 +33,15 @@ def test_abs(self): model = cp.Model(constraints) self.assertFalse(model.solve()) + #with list + constraints = [cp.abs(iv+2) <= 8, iv < 0] + model = cp.Model(constraints) + self.assertTrue(model.solve()) + + constraints = [cp.abs([iv[0], iv[2], iv[1]]) <= 8, iv < 0] + model = cp.Model(constraints) + self.assertTrue(model.solve()) + model = cp.Model(cp.abs(iv[0]).decompose_comparison('!=', 4)) self.assertTrue(model.solve()) self.assertNotEqual(str(cp.abs(iv[0].value())), '4') \ No newline at end of file From e582d68fdac589184f394cc12f547f885211d854 Mon Sep 17 00:00:00 2001 From: wout4 Date: Thu, 19 Sep 2024 09:31:13 +0200 Subject: [PATCH 09/12] add testcase --- tests/test_builtins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_builtins.py b/tests/test_builtins.py index c49cb298f..5c95b13aa 100644 --- a/tests/test_builtins.py +++ b/tests/test_builtins.py @@ -38,7 +38,7 @@ def test_abs(self): model = cp.Model(constraints) self.assertTrue(model.solve()) - constraints = [cp.abs([iv[0], iv[2], iv[1]]) <= 8, iv < 0] + constraints = [cp.abs([iv[0], iv[2], iv[1], -8]) <= 8, iv < 0] model = cp.Model(constraints) self.assertTrue(model.solve()) From cac7c26bbbe45c90d29f6454f49c0d17e060d6c1 Mon Sep 17 00:00:00 2001 From: wout4 Date: Fri, 20 Sep 2024 10:31:53 +0200 Subject: [PATCH 10/12] fix missing 'not' remove redundant 'abs' code --- cpmpy/expressions/core.py | 4 ---- cpmpy/expressions/python_builtins.py | 2 +- cpmpy/expressions/variables.py | 10 ---------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index 47d03be41..6fd426641 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -355,10 +355,6 @@ def __neg__(self): def __pos__(self): return self - def __abs__(self): - from .globalfunctions import Abs - return Abs(self) - def __invert__(self): if not (is_boolexpr(self)): raise TypeError("Not operator is only allowed on boolean expressions: {0}".format(self)) diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index a7c524932..0041ef9bd 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -139,7 +139,7 @@ def abs(element): if is_any_list(element): return cpm_array([abs(elem) for elem in element]) - if isinstance(element, Expression): + if not isinstance(element, Expression): return builtins.abs(element) return Abs(element) diff --git a/cpmpy/expressions/variables.py b/cpmpy/expressions/variables.py index 6c70e3eb0..b21018b88 100644 --- a/cpmpy/expressions/variables.py +++ b/cpmpy/expressions/variables.py @@ -309,12 +309,6 @@ def __init__(self, lb, ub, name=None): super().__init__(int(lb), int(ub), name=name) # explicit cast: can be numpy - # special casing for intvars (and boolvars) - def __abs__(self): - if self.lb >= 0: - # no-op - return self - return super().__abs__() class _BoolVarImpl(_IntVarImpl): @@ -342,8 +336,6 @@ def is_bool(self): def __invert__(self): return NegBoolView(self) - def __abs__(self): - return self # when redefining __eq__, must redefine custom__hash__ # https://stackoverflow.com/questions/53518981/inheritance-hash-sets-to-none-in-a-subclass @@ -652,8 +644,6 @@ def __ge__(self, other): # VECTORIZED math operators # only 'abs' 'neg' and binary ones # '~' not needed, gets translated to ==0 and that is already handled - def __abs__(self): - return cpm_array([abs(s) for s in self]) def __neg__(self): return cpm_array([-s for s in self]) From 8d299ab7a3533bb4099e2c303429737a64801754 Mon Sep 17 00:00:00 2001 From: wout4 Date: Fri, 20 Sep 2024 11:27:28 +0200 Subject: [PATCH 11/12] still overwrite __abs__ in expressions --- cpmpy/expressions/core.py | 4 ++++ cpmpy/expressions/variables.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index 6fd426641..47d03be41 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -355,6 +355,10 @@ def __neg__(self): def __pos__(self): return self + def __abs__(self): + from .globalfunctions import Abs + return Abs(self) + def __invert__(self): if not (is_boolexpr(self)): raise TypeError("Not operator is only allowed on boolean expressions: {0}".format(self)) diff --git a/cpmpy/expressions/variables.py b/cpmpy/expressions/variables.py index b21018b88..6c70e3eb0 100644 --- a/cpmpy/expressions/variables.py +++ b/cpmpy/expressions/variables.py @@ -309,6 +309,12 @@ def __init__(self, lb, ub, name=None): super().__init__(int(lb), int(ub), name=name) # explicit cast: can be numpy + # special casing for intvars (and boolvars) + def __abs__(self): + if self.lb >= 0: + # no-op + return self + return super().__abs__() class _BoolVarImpl(_IntVarImpl): @@ -336,6 +342,8 @@ def is_bool(self): def __invert__(self): return NegBoolView(self) + def __abs__(self): + return self # when redefining __eq__, must redefine custom__hash__ # https://stackoverflow.com/questions/53518981/inheritance-hash-sets-to-none-in-a-subclass @@ -644,6 +652,8 @@ def __ge__(self, other): # VECTORIZED math operators # only 'abs' 'neg' and binary ones # '~' not needed, gets translated to ==0 and that is already handled + def __abs__(self): + return cpm_array([abs(s) for s in self]) def __neg__(self): return cpm_array([-s for s in self]) From dc3ba3795d44c86c15c97c4b5b663faeb303edd1 Mon Sep 17 00:00:00 2001 From: Tias Guns Date: Thu, 3 Oct 2024 12:29:50 +0200 Subject: [PATCH 12/12] Update python_builtins.py bit more explicit... --- cpmpy/expressions/python_builtins.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cpmpy/expressions/python_builtins.py b/cpmpy/expressions/python_builtins.py index 0041ef9bd..140669a09 100644 --- a/cpmpy/expressions/python_builtins.py +++ b/cpmpy/expressions/python_builtins.py @@ -136,10 +136,13 @@ def abs(element): if the element given is not a CPMpy expression, the built-in is called else an Absolute functional global constraint is constructed. """ - if is_any_list(element): + if is_any_list(element): # compat: not allowed by builtins.abs(), but allowed by numpy.abs() return cpm_array([abs(elem) for elem in element]) - if not isinstance(element, Expression): - return builtins.abs(element) + if isinstance(element, Expression): + # create global + return Abs(element) + + return builtins.abs(element) - return Abs(element) +