Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 22 additions & 1 deletion cpmpy/solvers/exact.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
from .solver_interface import SolverInterface, SolverStatus, ExitStatus, Callback
from ..expressions.core import Expression, Comparison, Operator, BoolVal
from ..expressions.globalfunctions import Multiplication
from ..expressions.variables import intvar, _BoolVarImpl, NegBoolView, _IntVarImpl, _NumVarImpl
from ..expressions.variables import intvar, boolvar, _BoolVarImpl, NegBoolView, _IntVarImpl, _NumVarImpl
from ..transformations.comparison import only_numexpr_equality
from ..transformations.flatten_model import flatten_constraint, flatten_objective
from ..transformations.get_variables import get_variables
Expand All @@ -67,6 +67,7 @@
from ..expressions.globalconstraints import DirectConstraint
from ..expressions.utils import flatlist, argvals, argval, is_any_list, is_num
from ..exceptions import NotSupportedError
from .utils import SolverLookup

import numpy as np
import numbers
Expand Down Expand Up @@ -652,6 +653,26 @@ def get_core(self):

# return cpm_variables corresponding to Exact core
return [self.assumption_dict[i][1] for i in self.xct_solver.getLastCore()]

@staticmethod
def _native_mus(soft, hard=[]):
# Create assumption variables and model with hard + (assumption -> soft)
from cpmpy.tools.explain.utils import make_assump_model # avoid circular import
m, soft, assumptions = make_assump_model(soft, hard)

s = SolverLookup.get("exact", m)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps it could be a classmethod, in which case the first argument becomes cls and you could replace this with cls(m). This shows the intention of the native_mus being executed with the current class.


assert not s.solve(assumptions=assumptions), "MUS: model must be UNSAT"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check whether exact can detect this itself? maybe put a comment if exact cannot detect it


# set up assumptions for exact
s.xct_solver.setAssumptions([(s.solver_var(v), 1) for v in assumptions])

# call native MUS extractor
_, res_xct = s.xct_solver.extractMUS()

# get the constraints back from the assumption variables
dmap = dict(zip(assumptions, soft))
return [dmap[boolvar(name=c)] for c in res_xct]


def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]):
Expand Down
13 changes: 13 additions & 0 deletions cpmpy/solvers/solver_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ def get_core(self):
Setting these literals to True makes the model UNSAT, setting any to False makes it SAT
"""
raise NotSupportedError("Solver does not support unsat core extraction")

@staticmethod
def _native_mus(self, soft, hard=[]):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove self

"""
For using the solver's internal MUS extractor

Args:
soft: List of soft constraints over which a MUS needs to be found
hard: List of hard constraints that always need to be satisfied

Returns a MUS.
"""
raise NotSupportedError("Solver does not support MUS extraction")


# shared helper functions
Expand Down
16 changes: 16 additions & 0 deletions cpmpy/tools/explain/mus.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- QuickXplain
- Optimal MUS
"""
import sys
import warnings
import numpy as np
import cpmpy as cp
Expand Down Expand Up @@ -60,6 +61,21 @@ def mus(soft, hard=[], solver="ortools"):

return [dmap[avar] for avar in core]

def mus_native(soft, hard=[], solver="exact"):
"""
Compute a MUS using a solver's native MUS extractor.

:param soft: soft constraints, list of expressions
:param hard: hard constraints, optional, list of expressions
:param solver: which solver to use (only `gurobi` and `exact` supported)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this an assert, no idea what error we will get if I call it with the wrong name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related, there's a SolverLookup method out there that will get you the class (it may also return a nice error which would avoid the assert).

"""

# get solver class
class_name = f"CPM_{solver}"
solver_class = getattr(sys.modules[__name__], class_name)

return solver_class._native_mus(soft, hard)


def quickxplain(soft, hard=[], solver="ortools"):
"""
Expand Down
7 changes: 6 additions & 1 deletion tests/test_tools_mus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import cpmpy as cp
from cpmpy.tools import mss_opt, marco, OCUSException
from cpmpy.tools.explain import mus, mus_naive, quickxplain, quickxplain_naive, optimal_mus, optimal_mus_naive, mss, mcs, ocus, ocus_naive
from cpmpy.tools.explain import mus, mus_naive, quickxplain, quickxplain_naive, optimal_mus, optimal_mus_naive, mss, mcs, ocus, ocus_naive, mus_native


class TestMus:
Expand Down Expand Up @@ -83,6 +83,11 @@ def test_wglobal(self):
assert len(ms) < len(cons)
assert not cp.Model(ms).solve()
# self.assertEqual(set(self.naive_func(cons)), set(cons[:2]))

class TestNativeMusExact(TestMus):
def setup_method(self):
self.mus_func = lambda soft, hard=[], solver="exact": mus_native(soft, hard=hard, solver="exact")
self.naive_func = mus_naive


class TestQuickXplain(TestMus):
Expand Down
Loading