diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 52ce6734b..8653e7736 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -6,21 +6,17 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest-xdist requests pandas if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install python-sat[pblib] - pip install z3-solver - pip install exact - pip install pysdd - pip install pychoco + pip install ".[test, z3, choco, exact, pysat, pysdd, choco, minizinc]" + pip install pypblib # dependency of pysat for PB constraints + pip install pytest-xdist sudo snap install minizinc --classic - pip install minizinc - name: Test with pytest run: | python -m pytest -n 4 tests/ diff --git a/changelog.md b/changelog.md index 1dee36de5..626376695 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,242 @@ # Change log +## 0.9.25 + +### Added +* **New solver**: IBM CP Optimizer [#576](https://github.com/CPMpy/cpmpy/pull/576) +* **New global**: `ShortTable` [#469](https://github.com/CPMpy/cpmpy/pull/469) +* `model.add()` as new default to add constraints to model (same behaviour as `+=`) [#640](https://github.com/CPMpy/cpmpy/pull/640) +* Easy install of all solvers through the `"all"` optional dependency, i.e. `pip install cpmpy[all]` [#665](https://github.com/CPMpy/cpmpy/pull/665) +* More complex variants of the Sudoku puzzle (in examples) [#577](https://github.com/CPMpy/cpmpy/pull/577) +* API summery sheet [#629](https://github.com/CPMpy/cpmpy/pull/629) + +* Linearisation for `div`, `abs` and `mod` [#516](https://github.com/CPMpy/cpmpy/pull/516) +* Linearisation for subtraction [#639](https://github.com/CPMpy/cpmpy/pull/639) +* `div` linearisation for Gurobi [#593](https://github.com/CPMpy/cpmpy/pull/593) +* Native cardinality constraints for PySAT [#588](https://github.com/CPMpy/cpmpy/pull/588) +* `wsum` support for PySAT + +* Support for constants in `Element` global function [#630](https://github.com/CPMpy/cpmpy/pull/630) + +* `SolverLookup` now has `.supported()` and `.print_status()` to get information on the available solvers on the current system [#641](https://github.com/CPMpy/cpmpy/pull/641) +* `solveAll()` now accepts solver-specific kwargs, just like `solve()` [#582](https://github.com/CPMpy/cpmpy/pull/582) + +* Optional dependencies for solvers and tools (setup.py) [#599](https://github.com/CPMpy/cpmpy/pull/599) +* Documentation for all CPMpy exceptions [#622](https://github.com/CPMpy/cpmpy/pull/622) + + + +### Changed +* Bumped minimal Python version from 3.7 to 3.8 [#575](https://github.com/CPMpy/cpmpy/pull/575) +* `mod` and `div` now default to integer division +* Improved reading of DIMACS formatted files [#587](https://github.com/CPMpy/cpmpy/pull/587) +* Avoid max-based decomposition for `abs` if possible [#595](https://github.com/CPMpy/cpmpy/pull/595) +* Cleanup semantics of Boolean Python builtins [#602](https://github.com/CPMpy/cpmpy/pull/602) +* Performance optimisation of `simplify_bool` [#592](https://github.com/CPMpy/cpmpy/pull/592) +* Ensure `AllDifferent` decomposition is linear [#614](https://github.com/CPMpy/cpmpy/pull/614) +* Much improved README +* Improved docs formatting, especially for the Python API docstrings [#603](https://github.com/CPMpy/cpmpy/pull/603) +* Improved API documentation of explanation tools [#512](https://github.com/CPMpy/cpmpy/pull/512) +* Better exceptions for explanation tools [#512](https://github.com/CPMpy/cpmpy/pull/512) +* Improve documentation of non-standard solver args passing for Exact [#616](https://github.com/CPMpy/cpmpy/pull/616) +* General documentation improvements [#619](https://github.com/CPMpy/cpmpy/pull/619), [#633](https://github.com/CPMpy/cpmpy/pull/633), [#634](https://github.com/CPMpy/cpmpy/pull/634) +* Skip subset of `PySAT` tests when optional dependency `pblib` is not available [#668](https://github.com/CPMpy/cpmpy/pull/668) + +### Fixed +* Linearisation with boolean constants [#581](https://github.com/CPMpy/cpmpy/pull/581) +* Linearisation of `AllDifferent` with integer constants [#547](https://github.com/CPMpy/cpmpy/pull/547) +* Side conditions for `Precedence` global [#589](https://github.com/CPMpy/cpmpy/pull/589) +* `simplify_bool` on non-CPMpy expressions [#626](https://github.com/CPMpy/cpmpy/pull/626) +* Handling of negative variables in objective during linearisation [#495](https://github.com/CPMpy/cpmpy/pull/495) +* Integers in `GCC` global for Choco-solver [#646](https://github.com/CPMpy/cpmpy/pull/646) +* `NValueExceptN` for single value range [#645](https://github.com/CPMpy/cpmpy/pull/645) +* Handling of empty clauses in GCS [#662](https://github.com/CPMpy/cpmpy/pull/662) +* Missing user vars when calling `solveAll()`, resulting in incorrect number of solutions [#609](https://github.com/CPMpy/cpmpy/pull/609) +* Consistent handling of non-positive `time_limit` [#642](https://github.com/CPMpy/cpmpy/pull/642) +* Added `setuptools` to required dependencies (can be missing on some installs) [#664](https://github.com/CPMpy/cpmpy/pull/664) +* DIMACS tempfiles on Windows [#601](https://github.com/CPMpy/cpmpy/pull/601) + +### Removed + +* Removed support for Python version 3.7 [#575](https://github.com/CPMpy/cpmpy/pull/575) + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.24...v0.9.25 + + + +## 0.9.24 + +### Release Notes + +#### Enhancements and Features +- **Safening of partial functions**: New transformation to safen partial function. [#515](https://github.com/CPMpy/cpmpy/pull/515) +- **Update Choco interface with native reification**: Improved the Choco solver interface with native reification support. [#526](https://github.com/CPMpy/cpmpy/pull/526) +- **Reals in objective function**: Added support for real numbers in the objective function. [#529](https://github.com/CPMpy/cpmpy/pull/529) +- **Better naive grow**: Enhanced the "naive grow" strategy for better performance. [#528](https://github.com/CPMpy/cpmpy/pull/528) +- **Blocks world**: Introduced a "blocks world" example for demonstration purposes. [#533](https://github.com/CPMpy/cpmpy/pull/533) +- **Examples Colab links**: Added direct Colab links to examples for easier experimentation. [#553](https://github.com/CPMpy/cpmpy/pull/553) +- **Circuit decomposition for all ranges**: Extended circuit decomposition to handle all ranges. [#424](https://github.com/CPMpy/cpmpy/pull/424) +- **Global function in Z3 objective**: Introduced global functions in Z3 solver objectives. [#560](https://github.com/CPMpy/cpmpy/pull/560) +- **Z3 auto subsolver**: Implemented automatic subsolver selection for Z3 when solving optimization problems. [#567](https://github.com/CPMpy/cpmpy/pull/567) +- **Parametrize solver tests**: Streamlined solver test cases using parameterization. [#572](https://github.com/CPMpy/cpmpy/pull/572) +- **Linearize power**: Added linearization for power operations. [#538](https://github.com/CPMpy/cpmpy/pull/538) +- **Improve Boolean normalization for PySAT**: Enhanced normalization of Boolean terms for PySAT. [#569](https://github.com/CPMpy/cpmpy/pull/569) + +#### Performance Improvements +- **Has subexpr optimization**: Skip transformations of leaf expressions for improved efficiency. [#532](https://github.com/CPMpy/cpmpy/pull/532) +- **Only implied speedup**: Optimized "only implied" handling for significant speedups. [#541](https://github.com/CPMpy/cpmpy/pull/541) +- **Distribute tests over CPUs**: Distributed tests over 4 CPUs, reducing runtime from `21m30s` to `8m45s`. [#571](https://github.com/CPMpy/cpmpy/pull/571) +- **ndvar_getitem optimization**: Improved `ndvar_getitem` by moving fetch casts to initialization. [#550](https://github.com/CPMpy/cpmpy/pull/550) +- **Remove inline imports**: Instead use `import as x` at top of file. [#542](https://github.com/CPMpy/cpmpy/pull/542) + +#### Bug Fixes +- **Remove broadcast in min/max**: Fixed issues when forwarding to built-in min/max functions. [#536](https://github.com/CPMpy/cpmpy/pull/536) +- **Convert numpy array in Table constraint**: Ensured proper conversion of NumPy arrays to lists in Table constraints. [#540](https://github.com/CPMpy/cpmpy/pull/540) +- **Clear values on UNSAT**: Added functionality to clear variable values when UNSAT is detected. [#523](https://github.com/CPMpy/cpmpy/pull/523) +- **Fix cpm_array with order='F'**: Resolved issues with `cpm_array()` when using column-major order. [#555](https://github.com/CPMpy/cpmpy/pull/555) +- **Car sequencing index fix**: Corrected indexing issues in the car sequencing problem. [#565](https://github.com/CPMpy/cpmpy/pull/565) + +#### Code Quality and Maintenance +- **Improve exception messages**: Enhanced clarity of exception messages and removed unused imports. [#539](https://github.com/CPMpy/cpmpy/pull/539) +- **Edits to the docs**: Updated documentation for clarity and completeness. [#530](https://github.com/CPMpy/cpmpy/pull/530) +- **Gurobi license check**: Separated Gurobi license checks into a distinct process. [#566](https://github.com/CPMpy/cpmpy/pull/566) +- **Standardize solver version checks**: Unified approach to checking solver version compatibility. [#568](https://github.com/CPMpy/cpmpy/pull/568) +- **Update requirements**: Upped our minimal python requirement from 3.6 to 3.7. [#573](https://github.com/CPMpy/cpmpy/pull/573) + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.23...v0.9.24 + + + +## 0.9.23 + +Quick release, because we want the updated tools to be available. + +### What's Changed +* Extension to tools: MARCO and SMUS +* Added tests for incremental solving and fixed incemental solving with objective in Exact +* Cumulative decomposition fix when capacity was numpy integer. + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.22...v0.9.23 + + + +## 0.9.22 + +### What's New +* New solver: GlasgowConstraintSolver (GCS) +* Upgraded to Exact 2 +* Minizinc print: easily extract MiniZinc and FlatZinc text from CPMpy model. +* Update TEMPLATE.py to make it clearer how to add new solvers. +* SolverLookup gives clear error message in stead of returning None +* allow kwargs in Model.solve() +* call python builtins for sum, abs, min and max without expressions in the arguments. +* All solvers now have a native_model() function, to allow native solver access. +* It's now possible to name multiple new variables at once, by providing the names in a list. +* Linearize transformation can now rewrite modulo (if multiplication is supported) +* Fix behaviour of "all", "any", "max", "min", "sum", "prod" on higher dimensional NDVarArrays (maintain dimensionality) +* Value function of expressions now always returns a python integer, where it could sometimes be a numpy integer. +* Fixed performance issue where all solver vars where seen as user vars when solving with MiniZinc + +### Documentation +* Overall improvement of documentation +* update documentation of 'comparison' transformation + +### New Contributors +Thanks to 2 new contributors! +* [@ThomSerg](https://github.com/ThomSerg) and [@sin3000x](https://github.com/sin3000x) + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.21...v0.9.22 + + + +## 0.9.21 + +### New Global constraints: +* Increasing, Decreasing, IncreasingStrict, DecreasingStrict +* Lexicographical ordering constraints: LexLess, LexLessEq, LexChainLess, LexChainLessEq +* Scheduling constraints Precedence and NoOverlap +* Closed version of GCC +* AllDiffExceptN, AllEqualExceptN +* Nvalues except n +* Negative table +* Among + +### Bug Fixes: +* count nested in a wsum crashed MiniZinc [#461](https://github.com/CPMpy/cpmpy/issues/461) +* AllDifferentExcept0 now correctly works with MiniZinc +* User variables that get simplified away during transformation steps are now saved. +* Add missing case in simplify bool transformation. + +### Quality of life +* Removed type restriction for InDomain +* Extending automatic testsuite +* Check if minizinc executable is installed + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.20...v0.9.21 + + + +## 0.9.20 + +### What's Changed +* Choco is now a tier 2 solver, available to use after installing the pychoco package! +* new DIMACS parser and writer +* SolverLookup is now a classmethod for easier use of custom solvers. +* Fixed a bug where expression bounds didn't get rounded to integers when creating an intvar +* Added a warning when expressions have non-integer bounds, as these will be rounded +* Fixed a bug in our helper function is_bool, now also recognises our BoolVal objects +* Updated our ortools and minizinc version requirements. + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.19..v0.9.20 + + + +## 0.9.19 + +### What's Changed +* Update on tools/subsets, add mcs and mss tools with grow-variants. +* Full propagation with exact +* Adding NValue global constraint +* Minizinc result now saved in solver object, this allows to access the solver statistics. + +### Documentation +* Update docs for tools. +* Docs on solver statistics +* Format solver api, and add missing links in docs +* Update version and copyright date +* Remove bug where python comment (#) got interpreted as a header. + +### Bug Fixes +* Properly handle reified global constraints for Minizinc +* Correctly handle global constraints with list-arguments of length 1 +* Add missing edge case in flatten +* Type check table constraint first argument, cannot be a constant. + +**Full Changelog**: https://github.com/CPMpy/cpmpy/compare/v0.9.18...v0.9.19 + + + +## 0.9.18 +Minor release with some bugfixes, we are trying to do monthly releases now, so they will be more concise. + +### What's new? +* get_bounds() helper function now works on arrays and lists, returning arrays (lists) of bounds +* Pysdd added to our automated GitHub tests +* Pysdd does not support Xor anymore. + +### Bugfixes +* Fixed Cumulative bug when capacity was a numpy int. +* Cumulative now works in Minizinc with only one task. +* Docs look good again +* Corrected default parameter value in docs +* Fixed visualisations in Nonogram and Room assignment notebook examples +* Adopted the new ORtools 9.8 names of tuneable parameters. + +## New Contributors +* [@KennyVDV](https://github.com/KennyVDV) made their first contribution by adding a new example: chess_position + + + ## 0.9.17 Some new solvers supported at tier 3, update to our transformations and bugfixes. diff --git a/cpmpy/__init__.py b/cpmpy/__init__.py index 74b2508a9..e1922b1d3 100644 --- a/cpmpy/__init__.py +++ b/cpmpy/__init__.py @@ -12,9 +12,9 @@ - `solvers`: CPMpy classes that translate a model into approriate calls of a solver's API - `transformations`: common methods for transforming expressions into other expressions, used by `solvers` modules to simplify/rewrite expressions """ -# Tias Guns, 2019-2024 +# Tias Guns, 2019-2025 -__version__ = "0.9.24" +__version__ = "0.9.25" from .expressions import * diff --git a/cpmpy/expressions/__init__.py b/cpmpy/expressions/__init__.py index 9f9a08765..3f6ed724e 100644 --- a/cpmpy/expressions/__init__.py +++ b/cpmpy/expressions/__init__.py @@ -24,7 +24,7 @@ from .globalconstraints import AllDifferent, AllDifferentExcept0, AllDifferentExceptN, AllEqual, AllEqualExceptN, Circuit, Inverse, Table, ShortTable, Xor, Cumulative, \ IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, \ LexLess, LexLessEq, LexChainLess, LexChainLessEq, Precedence, NoOverlap, \ - NegativeTable + NegativeTable, Regular 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 diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index 7f3583fce..cc1ac040d 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -430,6 +430,10 @@ def __invert__(self): raise TypeError("Not operator is only allowed on boolean expressions: {0}".format(self)) return Operator("not", [self]) + def __bool__(self): + raise ValueError(f"__bool__ should not be called on a CPMPy expression {self} as it will always return True\n" + "Do not use an expression as argument in an `if` statement and use cpmpy.any, cpmpy.max instead of python builtins\n" + "If you think this is an error, please report on github") class BoolVal(Expression): """ @@ -551,6 +555,12 @@ def __repr__(self): return "({}) {} ({})".format(self.args[0], self.name, self.args[1]) # if not: prettier printing without braces return "{} {} {}".format(self.args[0], self.name, self.args[1]) + + def __bool__(self): + # will be called when comparing elements in a container, but always with `==` + if self.name == "==": + return repr(self.args[0]) == repr(self.args[1]) + super().__bool__() # default to exception # return the value of the expression # optional, default: None diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 2e63b78e5..573055ef8 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -108,6 +108,7 @@ def my_circuit_decomp(self): Table ShortTable NegativeTable + Regular IfThenElse InDomain Xor @@ -375,9 +376,24 @@ def __init__(self, fwd, rev): super().__init__("inverse", [fwd, rev]) def decompose(self): + fwd, rev = self.args rev = cpm_array(rev) - return [cp.all(rev[x] == i for i, x in enumerate(fwd))], [] + + constraining, defining = [], [] + for i,x in enumerate(fwd): + if is_num(x) and not 0 <= x < len(rev): + return [cp.BoolVal(False)], [] # can never satisfy the Inverse constraint + + lb, ub = get_bounds(x) + if lb >= 0 and ub < len(rev): # safe, index is within bounds + constraining.append(rev[x] == i) + else: # partial! need safening here + is_defined, total_expr, toplevel = cp.transformations.safening._safen_range(rev[x], (0, len(rev)-1), 1) + constraining += [is_defined, total_expr == i] + defining += toplevel + + return constraining, defining def value(self): fwd = argvals(self.args[0]) @@ -459,7 +475,87 @@ def value(self): arrval = argvals(arr) tabval = argvals(tab) return arrval not in tabval + +class Regular(GlobalConstraint): + """ + Regular-constraint (or Automaton-constraint) + Takes as input a sequence of variables and a automaton representation using a transition table. + The constraint is satisfied if the sequence of variables corresponds to an accepting path in the automaton. + + The automaton is defined by a list of transitions, a starting node and a list of accepting nodes. + The transitions are represented as a list of tuples, where each tuple is of the form (id1, value, id2). + An id is an integer or string representing a state in the automaton, and value is an integer representing the value of the variable in the sequence. + The starting node is an integer or string representing the starting state of the automaton. + The accepting nodes are a list of integers or strings representing the accepting states of the automaton. + + Example: an automaton that accepts the language 0*10* (exactly 1 variable taking value 1) is defined as: + cp.Regular(array = cp.intvar(0,1, shape=4), + transitions = [("A",0,"A"), ("A",1,"B"), ("B",0,"C"), ("C",0,"C")], + start = "A", + accepting = ["C"]) + """ + def __init__(self, array, transitions, start, accepting): + array = flatlist(array) + if not all(isinstance(x, Expression) for x in array): + raise TypeError("The first argument of a regular constraint should only contain variables/expressions") + + if not is_any_list(transitions): + raise TypeError("The second argument of a regular constraint should be a list of transitions") + _node_type = type(transitions[0][0]) + for s,v,e in transitions: + if not isinstance(s, _node_type) or not isinstance(e, _node_type) or not isinstance(v, int): + raise TypeError(f"The second argument of a regular constraint should be a list of transitions ({_node_type}, int, {_node_type})") + if not isinstance(start, _node_type): + raise TypeError("The third argument of a regular constraint should be a node id") + if not (is_any_list(accepting) and all(isinstance(e, _node_type) for e in accepting)): + raise TypeError("The fourth argument of a regular constraint should be a list of node ids") + super().__init__("regular", [array, transitions, start, list(accepting)]) + + self.nodes = set() + self.trans_dict = {} + for s, v, e in transitions: + self.nodes.update([s,e]) + self.trans_dict[(s, v)] = e + self.nodes = sorted(self.nodes) + # normalize node_ids to be 0..n-1, allows for smaller domains + self.node_map = {n: i for i, n in enumerate(self.nodes)} + + def decompose(self): + # Decompose to transition table using Table constraints + + arr, transitions, start, accepting = self.args + lbs, ubs = get_bounds(arr) + lb, ub = min(lbs), max(ubs) + + transitions = [[self.node_map[n_in], v, self.node_map[n_out]] for n_in, v, n_out in transitions] + + # add a sink node for transitions that are not defined + sink = len(self.nodes) + transitions += [[self.node_map[n], v, sink] for n in self.nodes for v in range(lb, ub + 1) if (n, v) not in self.trans_dict] + transitions += [[sink, v, sink] for v in range(lb, ub + 1)] + + # keep track of current state when traversing the array + state_vars = intvar(0, sink, shape=len(arr)) + id_start = self.node_map[start] + # optimization: we know the entry node of the automaton, results in smaller table + defining = [Table([arr[0], state_vars[0]], [[v,e] for s,v,e in transitions if s == id_start])] + # define the rest of the automaton using transition table + defining += [Table([state_vars[i - 1], arr[i], state_vars[i]], transitions) for i in range(1, len(arr))] + + # constraint is satisfied iff last state is accepting + return [InDomain(state_vars[-1], [self.node_map[e] for e in accepting])], defining + + def value(self): + arr, transitions, start, accepting = self.args + arrval = [argval(a) for a in arr] + curr_node = start + for v in arrval: + if (curr_node, v) in self.trans_dict: + curr_node = self.trans_dict[curr_node, v] + else: + return False + return curr_node in accepting # syntax of the form 'if b then x == 9 else x == 0' is not supported (no override possible) # same semantic as CPLEX IfThenElse constraint @@ -512,11 +608,11 @@ def decompose(self): they should be enforced toplevel. """ expr, arr = self.args - lb, ub = expr.get_bounds() - + lb, ub = get_bounds(expr) + defining = [] #if expr is not a var - if not isinstance(expr,_IntVarImpl): + if not isinstance(expr,Expression): aux = intvar(lb, ub) defining.append(aux == expr) expr = aux @@ -869,6 +965,9 @@ def decompose(self): """ X, Y = cpm_array(self.args) + if len(X) == 0 == len(Y): + return [cp.BoolVal(False)], [] # based on the decomp, it's false... + bvar = boolvar(shape=(len(X) + 1)) # Constraint ensuring that each element in X is less than or equal to the corresponding element in Y, @@ -915,6 +1014,9 @@ def decompose(self): """ X, Y = cpm_array(self.args) + if len(X) == 0 == len(Y): + return [cp.BoolVal(False)], [] # based on the decomp, it's false... + bvar = boolvar(shape=(len(X) + 1)) defining = [bvar == ((X <= Y) & ((X < Y) | bvar[1:]))] defining.append(bvar[-1] == (X[-1] <= Y[-1])) diff --git a/cpmpy/expressions/globalfunctions.py b/cpmpy/expressions/globalfunctions.py index 86925a108..dba6b0f19 100644 --- a/cpmpy/expressions/globalfunctions.py +++ b/cpmpy/expressions/globalfunctions.py @@ -69,9 +69,9 @@ def decompose_comparison(self): import cpmpy as cp from ..exceptions import CPMpyException, IncompleteFunctionError, TypeError -from .core import Expression, Operator, Comparison +from .core import Expression, Operator from .variables import boolvar, intvar, cpm_array -from .utils import flatlist, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds, argvals +from .utils import flatlist, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds, argvals, get_bounds, implies class GlobalFunction(Expression): @@ -238,6 +238,7 @@ def element(arg_list): warnings.warn("Deprecated, use Element(arr,idx) instead, will be removed in stable version", DeprecationWarning) assert (len(arg_list) == 2), "Element expression takes 2 arguments: Arr, Idx" return Element(arg_list[0], arg_list[1]) + class Element(GlobalFunction): """ The 'Element' global constraint enforces that the result equals Arr[Idx] @@ -286,8 +287,15 @@ def decompose_comparison(self, cpm_op, cpm_rhs): """ 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)], [] + # Find where the array indices and the bounds of `idx` intersect + lb, ub = get_bounds(idx) + new_lb, new_ub = max(lb, 0), min(ub, len(arr) - 1) + cons=[] + # For every `i` in that intersection, post `(idx = i) -> idx=i -> arr[i] cpm_rhs`. + for i in range(new_lb, new_ub+1): + cons.append(implies(idx == i, eval_comparison(cpm_op, arr[i], cpm_rhs))) + cons+=[idx >= new_lb, idx <= new_ub] # also enforce the new bounds + return cons, [] # no auxiliary variables def __repr__(self): return "{}[{}]".format(self.args[0], self.args[1]) @@ -387,7 +395,7 @@ def decompose_comparison(self, cmp_op, cpm_rhs): constraints = [] # introduce boolvar for each possible value - bvars = boolvar(shape=(ub+1-lb)) + bvars = boolvar(shape=(ub+1-lb,)) # shape is tuple to ensure it is a 1D array args = cpm_array(self.args) # bvar is true if the value is taken by any variable @@ -439,7 +447,7 @@ def decompose_comparison(self, cmp_op, cpm_rhs): constraints = [] # introduce boolvar for each possible value - bvars = boolvar(shape=(ub + 1 - lb)) + bvars = boolvar(shape=(ub+1-lb,)) # shape is tuple to ensure it is a 1D array idx_of_n = n - lb if 0 <= idx_of_n < len(bvars): count_of_vals = cp.sum(bvars[:idx_of_n]) + cp.sum(bvars[idx_of_n+1:]) diff --git a/cpmpy/expressions/utils.py b/cpmpy/expressions/utils.py index 626af8293..9707d961f 100644 --- a/cpmpy/expressions/utils.py +++ b/cpmpy/expressions/utils.py @@ -28,7 +28,6 @@ eval_comparison get_bounds """ -import copy import cpmpy as cp import numpy as np @@ -98,7 +97,6 @@ def is_any_list(arg): """ return isinstance(arg, (list, tuple, np.ndarray)) - def flatlist(args): """ recursively flatten arguments into one single list """ @@ -125,7 +123,7 @@ 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 """ if hasattr(a, "value"): @@ -160,6 +158,11 @@ def eval_comparison(str_op, lhs, rhs): Especially useful in decomposition and transformation functions that already involve a comparison. """ + if isinstance(lhs, (np.integer, np.bool_)): + lhs = int(lhs) + if isinstance(rhs, (np.integer, np.bool_)): + rhs = int(rhs) + if str_op == '==': return lhs == rhs elif str_op == '!=': @@ -210,6 +213,17 @@ def get_bounds(expr): return int(expr), int(expr) return math.floor(expr), math.ceil(expr) +def implies(expr, other): + """ like :func:`~cpmpy.expressions.core.Expression.implies`, but also safe to use for non-expressions """ + if isinstance(expr, cp.expressions.core.Expression): + return expr.implies(other) + elif is_true_cst(expr): + return other + elif is_false_cst(expr): + return cp.BoolVal(True) + else: + return expr.implies(other) + # Specific stuff for ShortTabel global (should this be in globalconstraints.py instead?) STAR = "*" # define constant here def is_star(arg): @@ -217,5 +231,3 @@ def is_star(arg): Check if arg is star as used in the ShortTable global constraint """ return isinstance(arg, type(STAR)) and arg == STAR - - diff --git a/cpmpy/model.py b/cpmpy/model.py index 5e73cace1..7bcb3b6e0 100644 --- a/cpmpy/model.py +++ b/cpmpy/model.py @@ -80,7 +80,7 @@ def __init__(self, *args, minimize=None, maximize=None): self.minimize(minimize) - def __add__(self, con): + def add(self, con): """ Add one or more constraints to the model. @@ -115,6 +115,7 @@ def __add__(self, con): self.constraints.append(con) return self + __add__ = add # Make __add__() (for the += operation) be the same as add() def minimize(self, expr): diff --git a/cpmpy/solvers/TEMPLATE.py b/cpmpy/solvers/TEMPLATE.py index 0b39b92a0..6e859b344 100644 --- a/cpmpy/solvers/TEMPLATE.py +++ b/cpmpy/solvers/TEMPLATE.py @@ -1,16 +1,51 @@ #!/usr/bin/env python +#-*- coding:utf-8 -*- +## +## TEMPLATE.py +## """ - Template file for a new solver interface + Interface to TEMPLATE's API + + .. note:: + [GUIDELINE] Replace