-
Notifications
You must be signed in to change notification settings - Fork 31
Rc2 maxsat solver #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tias
wants to merge
17
commits into
master
Choose a base branch
from
rc2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Rc2 maxsat solver #729
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
14d3643
initial rc2 impl, not very well tested
tias 16fdc62
more rc2
tias a58591f
add rc2 tests, obj
tias 6d5c342
direct encoding 0 weight softlit bug
tias 17e1671
also negative offset direct encodings need a shift
tias abdf3d8
based on discussion with alexey (stratified does not work yet)
tias 0628054
work around rc2 stratified bug, actually nicer to have params in solve
tias 648583a
fix bug, put back regular example testers
tias 39bf373
back to original tsp
tias f26ce45
disable test_constraints for OPT only rc2
tias 21cbe16
fix tests
tias 6e45597
more uniform handling of 0's and negative weights
tias 234e2c6
add rc2 to index page
tias e9a4f72
add docs api rc2
tias 5197cb4
add rc2 to setup
tias 8ef2559
rc2: activate cse map for flatten obj
tias 52dcc82
Merge branch 'master' into rc2
hbierlee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,351 @@ | ||
| #!/usr/bin/env python | ||
| #-*- coding:utf-8 -*- | ||
| ## | ||
| ## pysat.py | ||
| ## | ||
| """ | ||
| Interface to PySAT's RC2 MaxSAT solver API | ||
|
|
||
| PySAT is a Python (2.7, 3.4+) toolkit, which aims at providing a simple and unified | ||
| interface to a number of state-of-art Boolean satisfiability (SAT) solvers. It also | ||
| includes the RC2 MaxSAT solver. | ||
| (see https://pysathq.github.io/) | ||
|
|
||
| .. warning:: | ||
| It does not support satisfaction, only optimization. | ||
|
|
||
| Always use :func:`cp.SolverLookup.get("rc2") <cpmpy.solvers.utils.SolverLookup.get>` to instantiate the solver object. | ||
|
|
||
| ============ | ||
| Installation | ||
| ============ | ||
|
|
||
| Requires that the 'python-sat' package is installed: | ||
|
|
||
| .. code-block:: console | ||
|
|
||
| $ pip install pysat | ||
|
|
||
| If you want to also solve pseudo-Boolean constraints, you should also install its optional dependency 'pypblib', as follows: | ||
|
|
||
| .. code-block:: console | ||
|
|
||
| $ pip install pypblib | ||
|
|
||
| See detailed installation instructions at: | ||
| https://pysathq.github.io/installation | ||
|
|
||
| The rest of this documentation is for advanced users. | ||
|
|
||
| =============== | ||
| List of classes | ||
| =============== | ||
|
|
||
| .. autosummary:: | ||
| :nosignatures: | ||
|
|
||
| CPM_rc2 | ||
|
|
||
| ============== | ||
| Module details | ||
| ============== | ||
| """ | ||
| from threading import Timer | ||
| from typing import Optional | ||
| import warnings | ||
| import pkg_resources | ||
|
|
||
| from .solver_interface import SolverInterface, SolverStatus, ExitStatus | ||
| from .pysat import CPM_pysat | ||
| from ..exceptions import NotSupportedError | ||
| from ..expressions.core import Comparison, Operator, BoolVal | ||
| from ..expressions.variables import _BoolVarImpl, _IntVarImpl, NegBoolView | ||
| from ..expressions.globalconstraints import DirectConstraint | ||
| from ..transformations.linearize import canonical_comparison, only_positive_coefficients | ||
| from ..expressions.utils import is_int, flatlist | ||
| from ..transformations.comparison import only_numexpr_equality | ||
| from ..transformations.decompose_global import decompose_in_tree | ||
| from ..transformations.get_variables import get_variables | ||
| from ..transformations.flatten_model import flatten_constraint, flatten_objective | ||
| from ..transformations.linearize import linearize_constraint | ||
| from ..transformations.normalize import toplevel_list, simplify_boolean | ||
| from ..transformations.reification import only_implies, only_bv_reifies, reify_rewrite | ||
| from ..transformations.int2bool import int2bool, _encode_int_var, _decide_encoding, IntVarEncDirect | ||
|
|
||
|
|
||
| class CPM_rc2(CPM_pysat): | ||
| """ | ||
| Interface to PySAT's RC2 MaxSAT solver API. | ||
|
|
||
| Creates the following attributes (see parent constructor for more): | ||
|
|
||
| - ``pysat_vpool``: a pysat.formula.IDPool for the variable mapping | ||
| - ``pysat_solver``: a pysat.examples.rc2.RC2() (or .RC2Stratified()) | ||
| - ``ivarmap``: a mapping from integer variables to their encoding for `int2bool` | ||
| - ``encoding``: the encoding used for `int2bool`, choose from ("auto", "direct", "order", or "binary"). Set to "auto" but can be changed in the solver object. | ||
|
|
||
| The :class:`~cpmpy.expressions.globalconstraints.DirectConstraint`, when used, calls a function on the ``pysat_solver`` object. | ||
|
|
||
| Documentation of the solver's own Python API: | ||
| https://pysathq.github.io/docs/html/api/examples/rc2.html | ||
|
|
||
| .. note:: | ||
| CPMpy uses 'model' to refer to a constraint specification, | ||
| the PySAT docs use 'model' to refer to a solution. | ||
|
|
||
| """ | ||
|
|
||
| @staticmethod | ||
| def supported(): | ||
| # try to import the package | ||
| try: | ||
| import pysat | ||
| # there is actually a non-related 'pysat' package | ||
| # while we need the 'python-sat' package, some more checks: | ||
| from pysat.formula import IDPool | ||
| from pysat.solvers import Solver | ||
| from pysat.examples import rc2 | ||
|
|
||
| from pysat import card | ||
| CPM_rc2._card = card # native | ||
|
|
||
| # try to import pypblib and avoid ever re-import by setting `_pb` | ||
| if not hasattr(CPM_rc2, ("_pb")): | ||
| try: | ||
| from pysat import pb # require pypblib | ||
| """The `pysat.pb` module if its dependency `pypblib` installed, `None` if we have not checked it yet, or `False` if we checked and it is *not* installed""" | ||
| CPM_rc2._pb = pb | ||
| except (ModuleNotFoundError, NameError): # pysat returns the wrong error type (latter i/o former) | ||
| CPM_rc2._pb = None # not installed, avoid reimporting | ||
|
|
||
| return True | ||
| except ModuleNotFoundError: | ||
| return False | ||
| except Exception as e: | ||
| raise e | ||
|
|
||
| def __init__(self, cpm_model=None, subsolver=None): | ||
| """ | ||
| Constructor of the native solver object | ||
|
|
||
| Requires a CPMpy model as input, and will create the corresponding | ||
| PySAT clauses and solver object | ||
|
|
||
| Only supports optimisation problems (MaxSAT) | ||
|
|
||
| Arguments: | ||
| cpm_model (Model(), optional): a CPMpy Model() | ||
| subsolver (None): ignored | ||
|
|
||
| """ | ||
| if not self.supported(): | ||
| raise ImportError("PySAT is not installed. The recommended way to install PySAT is with `pip install cpmpy[pysat]`, or `pip install python-sat` if you do not require `pblib` to encode (weighted) sums.") | ||
| if cpm_model and cpm_model.objective_ is None: | ||
| raise NotSupportedError("CPM_rc2: only optimisation, does not support satisfaction") | ||
|
|
||
| from pysat.formula import IDPool, WCNF | ||
|
|
||
| self.pysat_solver = WCNF() # not actually the solver... | ||
| # fix an inconsistent API | ||
| self.pysat_solver.add_clause = self.pysat_solver.append | ||
| self.pysat_solver.append_formula = self.pysat_solver.extend | ||
| self.pysat_solver.supports_atmost = lambda: False | ||
| # TODO: accepts native cardinality constraints, not sure how to make clear... | ||
|
|
||
| # objective value related | ||
| self.objective_ = None # pysat returns the 'cost' of unsatisfied soft clauses, we want the value of the satisfied ones | ||
|
|
||
| # initialise the native solver object | ||
| self.pysat_vpool = IDPool() | ||
| self.ivarmap = dict() # for the integer to boolean encoders | ||
| self.encoding = "auto" | ||
|
|
||
| # initialise everything else and post the constraints/objective (skip PySAT itself) | ||
| super(CPM_pysat, self).__init__(name="rc2", cpm_model=cpm_model) | ||
|
|
||
| @property | ||
| def native_model(self): | ||
| """ | ||
| Returns the solver's underlying native model (for direct solver access). | ||
| """ | ||
| return self.pysat_solver | ||
|
|
||
| def solve(self, time_limit=None, stratified=True, adapt=True, exhaust=True, minz=True, **kwargs): | ||
| """ | ||
| Call the RC2 MaxSAT solver | ||
|
|
||
| Arguments: | ||
| time_limit (float, optional): Maximum solve time in seconds. Auto-interrups in case the | ||
| runtime exceeds given time_limit. | ||
|
|
||
| .. warning:: | ||
| Warning: the time_limit is not very accurate at subsecond level | ||
| stratified (bool, optional): use the stratified solver for weighted maxsat (default: True) | ||
| adapt (bool, optional): detect and adapt intrinsic AtMost1 constraint (default: True) | ||
| exhaust (bool, optional): do core exhaustion (default: True) | ||
| minz (bool, optional): do heuristic core reduction (default: True) | ||
|
|
||
| The last 4 parameters default values were recommended by the PySAT authors, based on their MaxSAT Evaluation 2018 submission. | ||
| """ | ||
| from pysat.examples import rc2 | ||
|
|
||
| # ensure all vars are known to solver | ||
| self.solver_vars(list(self.user_vars)) | ||
|
|
||
| # the user vars are only the Booleans (e.g. to ensure solveAll behaves consistently) | ||
| user_vars = set() | ||
| for x in self.user_vars: | ||
| if isinstance(x, _BoolVarImpl): | ||
| user_vars.add(x) | ||
| else: | ||
| # extends set with encoding variables of `x` | ||
| user_vars.update(self.ivarmap[x.name].vars()) | ||
| self.user_vars = user_vars | ||
|
|
||
| # TODO: set time limit | ||
| if time_limit is not None: | ||
| # rc2 does not support it, also not interrupts like pysat does | ||
| # we will have to manage it externally, e.g in a subprocess or so | ||
| raise NotImplementedError("CPM_rc2: time limit not yet supported") | ||
|
|
||
| # determine subsolver | ||
| if stratified: | ||
| slv = rc2.RC2Stratified(self.pysat_solver, adapt=adapt, exhaust=exhaust, minz=minz, **kwargs) | ||
| else: | ||
| slv = rc2.RC2(self.pysat_solver, adapt=adapt, exhaust=exhaust, minz=minz, **kwargs) | ||
|
|
||
| sol = slv.compute() # return one solution | ||
|
|
||
| # new status, translate runtime | ||
| self.cpm_status = SolverStatus(self.name) | ||
| self.cpm_status.runtime = slv.oracle_time() | ||
|
|
||
| # translate exit status | ||
| if sol is None: | ||
| self.cpm_status.exitstatus = ExitStatus.UNSATISFIABLE | ||
| else: | ||
| self.cpm_status.exitstatus = ExitStatus.OPTIMAL | ||
|
|
||
| # translate solution values (of user specified variables only) | ||
| if sol is not None: | ||
| # fill in variable values | ||
| for cpm_var in self.user_vars: | ||
| if isinstance(cpm_var, _BoolVarImpl): | ||
| lit = self.solver_var(cpm_var) | ||
| if lit in sol: | ||
| cpm_var._value = True | ||
| else: # -lit in sol (=False) or not specified (=False) | ||
| cpm_var._value = False | ||
| elif isinstance(cpm_var, _IntVarImpl): | ||
| raise TypeError("user_vars should only contain Booleans") | ||
| else: | ||
| raise NotImplementedError(f"CPM_rc2: variable {cpm_var} not supported") | ||
|
|
||
| # Now assign the user integer variables using their encodings | ||
| # `ivarmap` also contains auxiliary variable, but they will be assigned 'None' as their encoding variables are assigned `None` | ||
| for enc in self.ivarmap.values(): | ||
| enc._x._value = enc.decode() | ||
|
|
||
| else: # clear values of variables | ||
| for cpm_var in self.user_vars: | ||
| cpm_var._value = None | ||
| for enc in self.ivarmap.values(): | ||
| enc._x._value = None | ||
|
|
||
| return sol is not None | ||
|
|
||
|
|
||
| def transform_objective(self, expr): | ||
| """ | ||
| Transform the objective to a list of (w,x) and a constant | ||
| """ | ||
| # add new user vars to the set | ||
| get_variables(expr, collect=self.user_vars) | ||
|
|
||
| # try to flatten the objective | ||
| (flat_obj, flat_cons) = flatten_objective(expr, csemap=self._csemap) | ||
| self.add(flat_cons) | ||
|
|
||
| weights, xs, const = [], [], 0 | ||
| # we assume flat_obj is a var, a sum or a wsum (over int and bool vars) | ||
| if isinstance(flat_obj, _IntVarImpl) or isinstance(flat_obj, NegBoolView): # includes _BoolVarImpl | ||
| weights = [1] | ||
| xs = [flat_obj] | ||
| elif flat_obj.name == "sum": | ||
| xs = flat_obj.args | ||
| weights = [1] * len(xs) | ||
| elif flat_obj.name == "wsum": | ||
| weights, xs = flat_obj.args | ||
| else: | ||
| raise NotImplementedError(f"CPM_rc2: Non supported objective {flat_obj} (yet?)") | ||
|
|
||
| # transform weighted integers to weighted sum of Booleans | ||
| new_weights, new_xs = [], [] | ||
| for w, x in zip(weights, xs): | ||
| if isinstance(x, _BoolVarImpl): | ||
| if w != 0: | ||
| new_weights.append(w) | ||
| new_xs.append(x) | ||
| elif isinstance(x, _IntVarImpl): | ||
| # replace the intvar with its linear encoding | ||
| # ensure encoding is created | ||
| self.solver_var(x) | ||
| enc = self.ivarmap[x.name] | ||
| tlst, tconst = enc.encode_term(w) | ||
| const += tconst | ||
| for encw, encx in tlst: | ||
| if encw != 0: | ||
| new_weights.append(encw) | ||
| new_xs.append(encx) | ||
| elif isinstance(x, int): | ||
| const += w*x | ||
| else: | ||
| raise NotImplementedError(f"CPM_rc2: Non supported term {w,x} in objective {flat_obj} (yet?)") | ||
|
|
||
| # positive weights only, flip negative | ||
| for i in range(len(new_weights)): # inline replace | ||
| assert new_weights[i] != 0, f"CPM_rc2: positive weights only, got {new_weights[i],new_xs[i]}" | ||
| if new_weights[i] < 0: # negative weight | ||
| # wi*vi == wi*(1-(~vi)) == wi + -wi*~vi # where wi is negative | ||
| const += new_weights[i] | ||
| new_weights[i] = -new_weights[i] | ||
| new_xs[i] = ~new_xs[i] | ||
|
|
||
| return new_weights, new_xs, const | ||
|
|
||
|
|
||
| def objective(self, expr, minimize): | ||
| """ | ||
| Post the given expression to the solver as objective to minimize/maximize. | ||
|
|
||
| Arguments: | ||
| expr: Expression, the CPMpy expression that represents the objective function | ||
| minimize: Bool, whether it is a minimization problem (True) or maximization problem (False) | ||
|
|
||
| """ | ||
| # XXX WARNING, not incremental! Can NOT overwrite the objective.... only append to it! | ||
| if self.objective_ is not None: | ||
| raise NotSupportedError("CPM_rc2: objective can only be set once") | ||
| self.objective_ = expr | ||
|
|
||
| # maxsat by default maximizes | ||
| if minimize: | ||
| expr = -expr | ||
|
|
||
| # transform the objective to a list of (w,x) and a constant | ||
| weights, xs, const = self.transform_objective(expr) | ||
| assert len(weights) == len(xs), f"CPM_rc2 objective: expected equal nr weights and vars, got {weights, xs}" | ||
| assert isinstance(const, int), f"CPM_rc2 objective: expected constant to be an integer, got {const}" | ||
| # we don't need to keep the constant, we will recompute the objective value | ||
|
|
||
| # post weighted literals | ||
| for wi,vi in zip(weights, xs): | ||
| assert wi > 0, f"CPM_rc2 objective: strictly positive weights only, got {wi,vi}" | ||
| self.pysat_solver.append([self.solver_var(vi)], weight=wi) | ||
|
|
||
|
|
||
| def objective_value(self): | ||
| """ | ||
| Get the objective value of the last optimisation problem | ||
| """ | ||
| return self.objective_.value() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't we have
only_positive_coefficientsfor this? Or are you not using that because it's aComparison?