class MCSls(object): """ Algorithm BLS for computing MCSes, augmented with "clause :math:`D`" calls. Given an unsatisfiable partial CNF formula, i.e. formula in the :class:`.WCNF` format, this class can be used to compute a given number of MCSes of the formula. The implementation follows the description of the basic linear search (BLS) algorithm description in [1]_. It can use any SAT solver available in PySAT. Additionally, the "clause :math:`D`" heuristic can be used when enumerating MCSes. The default SAT solver to use is ``m22`` (see :class:`.SolverNames`). The "clause :math:`D`" heuristic is disabled by default, i.e. ``use_cld`` is set to ``False``. Internal SAT solver's timer is also disabled by default, i.e. ``use_timer`` is ``False``. :param formula: unsatisfiable partial CNF formula :param use_cld: whether or not to use "clause :math:`D`" :param solver_name: SAT oracle name :param use_timer: whether or not to use SAT solver's timer :type formula: :class:`.WCNF` :type use_cld: bool :type solver_name: str :type use_timer: bool """ def __init__(self, formula, use_cld=False, solver_name='m22', use_timer=False): """ Constructor. """ # bootstrapping the solver with hard clauses self.oracle = Solver(name=solver_name, bootstrap_with=formula.hard, use_timer=use_timer) self.solver = solver_name # adding native cardinality constraints (if any) as hard clauses # this can be done only if the Minicard solver is in use if isinstance(formula, WCNFPlus) and formula.atms: assert solver_name in SolverNames.minicard, \ 'Only Minicard supports native cardinality constraints. Make sure you use the right type of formula.' for atm in formula.atms: self.oracle.add_atmost(*atm) self.topv = formula.nv # top variable id self.sels = [] self.ucld = use_cld self.smap = {} # mappings between internal and external variables VariableMap = collections.namedtuple('VariableMap', ['e2i', 'i2e']) self.vmap = VariableMap(e2i={}, i2e={}) # at this point internal and external variables are the same for v in range(1, formula.nv + 1): self.vmap.e2i[v] = v self.vmap.i2e[v] = v for cl in formula.soft: new_cl = cl[:] if len(cl) > 1 or cl[0] < 0: self.topv += 1 sel = self.topv new_cl.append(-sel) # creating a new selector self.oracle.add_clause(new_cl) else: sel = cl[0] self.sels.append(sel) self.smap[sel] = len(self.sels) def __del__(self): """ Destructor. """ self.delete() def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def delete(self): """ Explicit destructor of the internal SAT oracle. """ if self.oracle: self.oracle.delete() self.oracle = None def add_clause(self, clause, soft=False): """ The method for adding a new hard of soft clause to the problem formula. Although the input formula is to be specified as an argument of the constructor of :class:`MCSls`, adding clauses may be helpful when *enumerating* MCSes of the formula. This way, the clauses are added incrementally, i.e. *on the fly*. The clause to add can be any iterable over integer literals. The additional Boolean parameter ``soft`` can be set to ``True`` meaning the the clause being added is soft (note that parameter ``soft`` is set to ``False`` by default). :param clause: a clause to add :param soft: whether or not the clause is soft :type clause: iterable(int) :type soft: bool """ # first, map external literals to internal literals # introduce new variables if necessary cl = list( map( lambda l: self._map_extlit(l), clause if not len(clause) == 2 or not type(clause[0]) == list else clause[0])) if not soft: if not len(clause) == 2 or not type(clause[0]) == list: # the clause is hard, and so we simply add it to the SAT oracle self.oracle.add_clause(cl) else: # this should be a native cardinality constraint, # which can be used only together with Minicard assert self.solver in SolverNames.minicard, \ 'Only Minicard supports native cardinality constraints.' self.oracle.add_atmost(cl, clause[1]) else: # soft clauses should be augmented with a selector sel = cl[0] if len(cl) > 1 or cl[0] < 0: self.topv += 1 sel = self.topv self.oracle.add_clause(cl + [-sel]) self.sels.append(sel) self.smap[sel] = len(self.sels) def compute(self): """ Compute and return one solution. This method checks whether the hard part of the formula is satisfiable, i.e. an MCS can be extracted. If the formula is satisfiable, the model computed by the SAT call is used as an *over-approximation* of the MCS in the method :func:`_compute` invoked here, which implements the BLS algorithm augmented with CLD oracle calls. An MCS is reported as a list of integers, each representing a soft clause index (the smallest index is ``1``). :rtype: list(int) """ self.setd = [] self.solution = None self.bb_assumps = [] # backbone assumptions self.ss_assumps = [] # satisfied soft clause assumptions if self.oracle.solve(): # hard part is satisfiable => there is a solution self._overapprox() self._compute() self.solution = [self.smap[-l] for l in self.bb_assumps] return self.solution def enumerate(self): """ This method iterates through MCSes enumerating them until the formula has no more MCSes. The method iteratively invokes :func:`compute`. Note that the method does not block the MCSes computed - this should be explicitly done by a user. """ done = False while not done: mcs = self.compute() if mcs != None: yield mcs else: done = True def block(self, mcs): """ Block a (previously computed) MCS. The MCS should be given as an iterable of integers. Note that this method is not automatically invoked from :func:`enumerate` because a user may want to block some of the MCSes conditionally depending on the needs. For example, one may want to compute disjoint MCSes only in which case this standard blocking is not appropriate. :param mcs: an MCS to block :type mcs: iterable(int) """ self.oracle.add_clause([self.sels[cl_id - 1] for cl_id in mcs]) def _overapprox(self): """ The method extracts a model corresponding to an over-approximation of an MCS, i.e. it is the model of the hard part of the formula (the corresponding oracle call is made in :func:`compute`). Here, the set of selectors is divided into two parts: ``self.ss_assumps``, which is an under-approximation of an MSS (maximal satisfiable subset) and ``self.setd``, which is an over-approximation of the target MCS. Both will be further refined in :func:`_compute`. """ model = self.oracle.get_model() for sel in self.sels: if len(model) < sel or model[sel - 1] > 0: # soft clauses contain positive literals # so if var is true then the clause is satisfied self.ss_assumps.append(sel) else: self.setd.append(sel) def _compute(self): """ The main method of the class, which computes an MCS given its over-approximation. The over-approximation is defined by a model for the hard part of the formula obtained in :func:`_overapprox` (the corresponding oracle is made in :func:`compute`). The method is essentially a simple loop going over all literals unsatisfied by the previous model, i.e. the literals of ``self.setd`` and checking which literals can be satisfied. This process can be seen a refinement of the over-approximation of the MCS. The algorithm follows the pseudo-code of the BLS algorithm presented in [1]_. Additionally, if :class:`MCSls` was constructed with the requirement to make "clause :math:`D`" calls, the method calls :func:`do_cld_check` at every iteration of the loop using the literals of ``self.setd`` not yet checked, as the contents of "clause :math:`D`". """ # unless clause D checks are used, test one literal at a time # and add it either to satisfied of backbone assumptions i = 0 while i < len(self.setd): if self.ucld: self.do_cld_check(self.setd[i:]) i = 0 if self.setd: # if may be empty after the clause D check self.ss_assumps.append(self.setd[i]) if not self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps): self.ss_assumps.pop() self.bb_assumps.append(-self.setd[i]) i += 1 def do_cld_check(self, cld): """ Do the "clause :math:`D`" check. This method receives a list of literals, which serves a "clause :math:`D`" [1]_, and checks whether the formula conjoined with :math:`D` is satisfiable. If clause :math:`D` cannot be satisfied together with the formula, then negations of all of its literals are backbones of the formula and the MCSls algorithm can stop. Otherwise, the literals satisfied by the new model refine the MCS further. Every time the method is called, a new fresh selector variable :math:`s` is introduced, which augments the current clause :math:`D`. The SAT oracle then checks if clause :math:`(D \\vee \\neg{s})` can be satisfied together with the internal formula. The :math:`D` clause is then disabled by adding a hard clause :math:`(\\neg{s})`. :param cld: clause :math:`D` to check :type cld: list(int) """ # adding a selector literal to clause D # selector literals for clauses D currently # cannot be reused, but this may change later self.topv += 1 sel = self.topv cld.append(-sel) # adding clause D self.oracle.add_clause(cld) self.ss_assumps.append(sel) self.setd = [] self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps) self.ss_assumps.pop() # removing clause D assumption if self.oracle.get_status() == True: model = self.oracle.get_model() for l in cld[:-1]: # filtering all satisfied literals if model[abs(l) - 1] > 0: self.ss_assumps.append(l) else: self.setd.append(l) else: # clause D is unsatisfiable => all literals are backbones self.bb_assumps.extend([-l for l in cld[:-1]]) # deactivating clause D self.oracle.add_clause([-sel]) def _map_extlit(self, l): """ Map an external variable to an internal one if necessary. This method is used when new clauses are added to the formula incrementally, which may result in introducing new variables clashing with the previously used *clause selectors*. The method makes sure no clash occurs, i.e. it maps the original variables used in the new problem clauses to the newly introduced auxiliary variables (see :func:`add_clause`). Given an integer literal, a fresh literal is returned. The returned integer has the same sign as the input literal. :param l: literal to map :type l: int :rtype: int """ v = abs(l) if v in self.vmap.e2i: return int(copysign(self.vmap.e2i[v], l)) else: self.topv += 1 self.vmap.e2i[v] = self.topv self.vmap.i2e[self.topv] = v return int(copysign(self.topv, l)) def oracle_time(self): """ Report the total SAT solving time. """ return self.oracle.time_accum()
class FM(object): """ A non-incremental implementation of the FM (Fu&Malik, or WMSU1) algorithm. The algorithm (see details in [5]_) is *core-guided*, i.e. it solves maximum satisfiability with a series of unsatisfiability oracle calls, each producing an unsatisfiable core. The clauses involved in an unsatisfiable core are *relaxed* and a new :math:`\\textsf{AtMost1}` constraint on the corresponding *relaxation variables* is added to the formula. The process gets a bit more sophisticated in the case of weighted formulas because of the *clause weight splitting* technique. The constructor of :class:`FM` objects receives a target :class:`.WCNF` MaxSAT formula, an identifier of the cardinality encoding to use, a SAT solver name, and a verbosity level. Note that the algorithm uses the ``pairwise`` (see :class:`.card.EncType`) cardinality encoding by default, while the default SAT solver is MiniSat22 (referred to as ``'m22'``, see :class:`.SolverNames` for details). The default verbosity level is ``1``. :param formula: input MaxSAT formula :param enc: cardinality encoding to use :param solver: name of SAT solver :param verbose: verbosity level :type formula: :class:`.WCNF` :type enc: int :type solver: str :type verbose: int """ def __init__(self, formula, enc=EncType.pairwise, solver='m22', verbose=1): """ Constructor. """ # saving verbosity level self.verbose = verbose self.solver = solver self.time = 0.0 # MaxSAT related stuff self.topv = self.orig_nv = formula.nv self.hard = copy.deepcopy(formula.hard) self.soft = copy.deepcopy(formula.soft) self.wght = formula.wght[:] self.cenc = enc self.cost = 0 if isinstance(formula, WCNFPlus) and formula.atms: self.atm1 = copy.deepcopy(formula.atms) else: self.atm1 = None # initialize SAT oracle with hard clauses only self.init(with_soft=False) def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def init(self, with_soft=True): """ The method for the SAT oracle initialization. Since the oracle is is used non-incrementally, it is reinitialized at every iteration of the MaxSAT algorithm (see :func:`reinit`). An input parameter ``with_soft`` (``False`` by default) regulates whether or not the formula's soft clauses are copied to the oracle. :param with_soft: copy formula's soft clauses to the oracle or not :type with_soft: bool """ self.oracle = Solver(name=self.solver, bootstrap_with=self.hard, use_timer=True) if self.atm1: # this check is needed at the beggining (before iteration 1) assert self.oracle.supports_atmost(), \ '{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(solver_name) # self.atm1 is not empty only in case of minicard for am in self.atm1: self.oracle.add_atmost(*am) if with_soft: for cl, cpy in zip(self.soft, self.scpy): if cpy: self.oracle.add_clause(cl) def delete(self): """ Explicit destructor of the internal SAT oracle. """ if self.oracle: self.time += self.oracle.time_accum() # keep SAT solving time self.oracle.delete() self.oracle = None def reinit(self): """ This method calls :func:`delete` and :func:`init` to reinitialize the internal SAT oracle. This is done at every iteration of the MaxSAT algorithm. """ self.delete() self.init(); def compute(self): """ Compute a MaxSAT solution. First, the method checks whether or not the set of hard clauses is satisfiable. If not, the method returns ``False``. Otherwise, add soft clauses to the oracle and call the MaxSAT algorithm (see :func:`_compute`). Note that the soft clauses are added to the oracles after being augmented with additional *selector* literals. The selectors literals are then used as *assumptions* when calling the SAT oracle and are needed for extracting unsatisfiable cores. """ if self.oracle.solve(): # hard part is satisfiable # create selectors and a mapping from selectors to clause ids self.sels, self.vmap = [], {} self.scpy = [True for cl in self.soft] # adding soft clauses to oracle for i in range(len(self.soft)): self.topv += 1 self.soft[i].append(-self.topv) self.sels.append(self.topv) self.oracle.add_clause(self.soft[i]) self.vmap[self.topv] = i self._compute() return True else: return False def _compute(self): """ This method implements WMSU1 algorithm. The method is essentially a loop, which at each iteration calls the SAT oracle to decide whether the working formula is satisfiable. If it is, the method derives a model (stored in variable ``self.model``) and returns. Otherwise, a new unsatisfiable core of the formula is extracted and processed (see :func:`treat_core`), and the algorithm proceeds. """ while True: if self.oracle.solve(assumptions=self.sels): self.model = self.oracle.get_model() self.model = list(filter(lambda l: abs(l) <= self.orig_nv, self.model)) return else: self.treat_core() if self.verbose > 1: print('c cost: {0}; core sz: {1}'.format(self.cost, len(self.core))) self.reinit() def treat_core(self): """ Now that the previous SAT call returned UNSAT, a new unsatisfiable core should be extracted and relaxed. Core extraction is done through a call to the :func:`pysat.solvers.Solver.get_core` method, which returns a subset of the selector literals deemed responsible for unsatisfiability. After the core is extracted, its *minimum weight* ``minw`` is computed, i.e. it is the minimum weight among the weights of all soft clauses involved in the core (see [5]_). Note that the cost of the MaxSAT solution is incremented by ``minw``. Clauses that have weight larger than ``minw`` are split (see :func:`split_core`). Afterwards, all clauses of the unsatisfiable core are relaxed (see :func:`relax_core`). """ # extracting the core self.core = [self.vmap[sel] for sel in self.oracle.get_core()] minw = min(map(lambda i: self.wght[i], self.core)) # updating the cost self.cost += minw # splitting clauses in the core if necessary self.split_core(minw) # relaxing clauses in the core and adding a new atmost1 constraint self.relax_core() def split_core(self, minw): """ Split clauses in the core whenever necessary. Given a list of soft clauses in an unsatisfiable core, the method is used for splitting clauses whose weights are greater than the minimum weight of the core, i.e. the ``minw`` value computed in :func:`treat_core`. Each clause :math:`(c\\vee\\neg{s},w)`, s.t. :math:`w>minw` and :math:`s` is its selector literal, is split into clauses (1) clause :math:`(c\\vee\\neg{s}, minw)` and (2) a residual clause :math:`(c\\vee\\neg{s}',w-minw)`. Note that the residual clause has a fresh selector literal :math:`s'` different from :math:`s`. :param minw: minimum weight of the core :type minw: int """ for clid in self.core: sel = self.sels[clid] if self.wght[clid] > minw: self.topv += 1 cl_new = [] for l in self.soft[clid]: if l != -sel: cl_new.append(l) else: cl_new.append(-self.topv) self.sels.append(self.topv) self.vmap[self.topv] = len(self.soft) self.soft.append(cl_new) self.wght.append(self.wght[clid] - minw) self.wght[clid] = minw self.scpy.append(True) def relax_core(self): """ Relax and bound the core. After unsatisfiable core splitting, this method is called. If the core contains only one clause, i.e. this clause cannot be satisfied together with the hard clauses of the formula, the formula gets augmented with the negation of the clause (see :func:`remove_unit_core`). Otherwise (if the core contains more than one clause), every clause :math:`c` of the core is *relaxed*. This means a new *relaxation literal* is added to the clause, i.e. :math:`c\gets c\\vee r`, where :math:`r` is a fresh (unused) relaxation variable. After the clauses get relaxed, a new cardinality encoding is added to the formula enforcing the sum of the new relaxation variables to be not greater than 1, :math:`\sum_{c\in\phi}{r\leq 1}`, where :math:`\phi` denotes the unsatisfiable core. """ if len(self.core) > 1: # relaxing rels = [] for clid in self.core: self.topv += 1 rels.append(self.topv) self.soft[clid].append(self.topv) # creating a new cardinality constraint am1 = CardEnc.atmost(lits=rels, top_id=self.topv, encoding=self.cenc) for cl in am1.clauses: self.hard.append(cl) # only if minicard # (for other solvers am1.atmosts should be empty) for am in am1.atmosts: self.atm1.append(am) self.topv = am1.nv elif len(self.core) == 1: # unit core => simply negate the clause self.remove_unit_core() def remove_unit_core(self): """ If an unsatisfiable core contains only one clause :math:`c`, this method is invoked to add a bunch of new unit size hard clauses. As a result, the SAT oracle gets unit clauses :math:`(\\neg{l})` for all literals :math:`l` in clause :math:`c`. """ self.scpy[self.core[0]] = False for l in self.soft[self.core[0]]: self.hard.append([-l]) def oracle_time(self): """ Method for calculating and reporting the total SAT solving time. """ self.time += self.oracle.time_accum() # include time of the last SAT call return self.time
class MUSX(object): """ MUS eXtractor using the deletion-based algorithm. The algorithm is described in [1]_ (also see the module description above). Essentially, the algorithm can be seen as an iterative process, which tries to remove one soft clause at a time and check whether the remaining set of soft clauses is still unsatisfiable together with the hard clauses. The constructor of :class:`MUSX` objects receives a target :class:`.WCNF` formula, a SAT solver name, and a verbosity level. Note that the default SAT solver is MiniSat22 (referred to as ``'m22'``, see :class:`.SolverNames` for details). The default verbosity level is ``1``. :param formula: input WCNF formula :param solver: name of SAT solver :param verbosity: verbosity level :type formula: :class:`.WCNF` :type solver: str :type verbosity: int """ def __init__(self, formula, solver='m22', verbosity=1): """ Constructor. """ topv, self.verbose = formula.nv, verbosity # clause selectors and a mapping from selectors to clause ids self.sels, self.vmap = [], {} # constructing the oracle self.oracle = Solver(name=solver, bootstrap_with=formula.hard, use_timer=True) if isinstance(formula, WCNFPlus) and formula.atms: assert solver in SolverNames.minicard, \ 'Only Minicard supports native cardinality constraints. Make sure you use the right type of formula.' for atm in formula.atms: self.oracle.add_atmost(*atm) # relaxing soft clauses and adding them to the oracle for i, cl in enumerate(formula.soft): topv += 1 self.sels.append(topv) self.vmap[topv] = i self.oracle.add_clause(cl + [-topv]) def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.oracle.delete() self.oracle = None def delete(self): """ Explicit destructor of the internal SAT oracle. """ if self.oracle: self.oracle.delete() self.oracle = None def compute(self): """ This is the main method of the :class:`MUSX` class. It computes a set of soft clauses belonging to an MUS of the input formula. First, the method checks whether the formula is satisfiable. If it is, nothing else is done. Otherwise, an *unsatisfiable core* of the formula is extracted, which is later used as an over-approximation of an MUS refined in :func:`_compute`. """ # cheking whether or not the formula is unsatisfiable if not self.oracle.solve(assumptions=self.sels): # get an overapproximation of an MUS approx = sorted(self.oracle.get_core()) if self.verbose: print('c MUS approx:', ' '.join([str(self.vmap[sel] + 1) for sel in approx]), '0') # iterate over clauses in the approximation and try to delete them mus = self._compute(approx) # return an MUS return list(map(lambda x: self.vmap[x] + 1, mus)) def _compute(self, approx): """ Deletion-based MUS extraction. Given an over-approximation of an MUS, i.e. an unsatisfiable core previously returned by a SAT oracle, the method represents a loop, which at each iteration removes a clause from the core and checks whether the remaining clauses of the approximation are unsatisfiable together with the hard clauses. Soft clauses are (de)activated using the standard MiniSat-like assumptions interface [2]_. Each soft clause :math:`c` is augmented with a selector literal :math:`s`, e.g. :math:`(c) \gets (c \\vee \\neg{s})`. As a result, clause :math:`c` can be activated by assuming literal :math:`s`. The over-approximation provided as an input is specified as a list of selector literals for clauses in the unsatisfiable core. .. [2] Niklas Eén, Niklas Sörensson. *Temporal induction by incremental SAT solving*. Electr. Notes Theor. Comput. Sci. 89(4). 2003. pp. 543-560 :param approx: an over-approximation of an MUS :type approx: list(int) Note that the method does not return. Instead, after its execution, the input over-approximation is refined and contains an MUS. """ i = 0 while i < len(approx): to_test = approx[:i] + approx[(i + 1):] sel, clid = approx[i], self.vmap[approx[i]] if self.verbose > 1: print('c testing clid: {0}'.format(clid), end='') if self.oracle.solve(assumptions=to_test): if self.verbose > 1: print(' -> sat (keeping {0})'.format(clid)) i += 1 else: if self.verbose > 1: print(' -> unsat (removing {0})'.format(clid)) approx = to_test return approx def oracle_time(self): """ Method for calculating and reporting the total SAT solving time. """ return self.oracle.time_accum()
class FM(object): """ Algorithm FM - FU & Malik - MSU1. """ def __init__(self, formula, enc=EncType.pairwise, solver='m22', verbose=1): """ Constructor. """ # saving verbosity level self.verbose = verbose self.solver = solver self.time = 0.0 # MaxSAT related stuff self.topv = self.orig_nv = formula.nv self.hard = formula.hard self.soft = formula.soft self.atm1 = [] self.wght = formula.wght self.cenc = enc self.cost = 0 # initialize SAT oracle with hard clauses only self.init(with_soft=False) def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def init(self, with_soft=True): """ Initialize the SAT solver. """ self.oracle = Solver(name=self.solver, bootstrap_with=self.hard, use_timer=True) # self.atm1 is not empty only in case of minicard for am in self.atm1: self.oracle.add_atmost(*am) if with_soft: for cl, cpy in zip(self.soft, self.scpy): if cpy: self.oracle.add_clause(cl) def delete(self): """ Explicit destructor. """ if self.oracle: self.time += self.oracle.time_accum() # keep SAT solving time self.oracle.delete() self.oracle = None def reinit(self): """ Delete and create a new SAT solver. """ self.delete() self.init() def compute(self): """ Compute and return a solution. """ if self.oracle.solve(): # hard part is satisfiable # create selectors and a mapping from selectors to clause ids self.sels, self.vmap = [], {} self.scpy = [True for cl in self.soft] # adding soft clauses to oracle for i in range(len(self.soft)): self.topv += 1 self.soft[i].append(-self.topv) self.sels.append(self.topv) self.oracle.add_clause(self.soft[i]) self.vmap[self.topv] = i self._compute() else: print('s UNSATISFIABLE') def _compute(self): """ Compute and return a solution. """ while True: if self.oracle.solve(assumptions=self.sels): print('s OPTIMUM FOUND') print('o {0}'.format(self.cost)) if self.verbose > 1: model = self.oracle.get_model() model = filter(lambda l: abs(l) <= self.orig_nv, model) print('v', ' '.join([str(l) for l in model]), '0') return else: self.treat_core() if self.verbose: print('c cost: {0}; core sz: {1}'.format( self.cost, len(self.core))) self.reinit() def treat_core(self): """ Found core in main loop, deal with it. """ # extracting the core self.core = [self.vmap[sel] for sel in self.oracle.get_core()] minw = min(map(lambda i: self.wght[i], self.core)) # updating the cost self.cost += minw # splitting clauses in the core if necessary self.split_core(minw) # relaxing clauses in the core and adding a new atmost1 constraint self.relax_core() def split_core(self, minw): """ Split clauses in the core whenever necessary. """ for clid in self.core: sel = self.sels[clid] if self.wght[clid] > minw: self.topv += 1 cl_new = [] for l in self.soft[clid]: if l != -sel: cl_new.append(l) else: cl_new.append(-self.topv) self.sels.append(self.topv) self.vmap[self.topv] = len(self.soft) self.soft.append(cl_new) self.wght.append(self.wght[clid] - minw) self.wght[clid] = minw self.scpy.append(True) def relax_core(self): """ Relax and bound the core. """ if len(self.core) > 1: # relaxing rels = [] for clid in self.core: self.topv += 1 rels.append(self.topv) self.soft[clid].append(self.topv) # creating a new cardinality constraint am1 = CardEnc.atmost(lits=rels, top_id=self.topv, encoding=self.cenc) for cl in am1.clauses: self.hard.append(cl) # only if minicard # (for other solvers am1.atmosts should be empty) for am in am1.atmosts: self.atm1.append(am) self.topv = am1.nv elif len(self.core) == 1: # unit core => simply negate the clause self.remove_unit_core() def remove_unit_core(self): """ Remove a clause responsible for a unit core. """ self.scpy[self.core[0]] = False for l in self.soft[self.core[0]]: self.hard.append([-l]) def oracle_time(self): """ Report the total SAT solving time. """ self.time += self.oracle.time_accum( ) # include time of the last SAT call return self.time
class LBX(object): """ LBX-like algorithm for computing MCSes. Given an unsatisfiable partial CNF formula, i.e. formula in the :class:`.WCNF` format, this class can be used to compute a given number of MCSes of the formula. The implementation follows the LBX algorithm description in [1]_. It can use any SAT solver available in PySAT. Additionally, the "clause :math:`D`" heuristic can be used when enumerating MCSes. The default SAT solver to use is ``m22`` (see :class:`.SolverNames`). The "clause :math:`D`" heuristic is disabled by default, i.e. ``use_cld`` is set to ``False``. Internal SAT solver's timer is also disabled by default, i.e. ``use_timer`` is ``False``. :param formula: unsatisfiable partial CNF formula :param use_cld: whether or not to use "clause :math:`D`" :param solver_name: SAT oracle name :param use_timer: whether or not to use SAT solver's timer :type formula: :class:`.WCNF` :type use_cld: bool :type solver_name: str :type use_timer: bool """ def __init__(self, formula, use_cld=False, solver_name='m22', use_timer=False): """ Constructor. """ # bootstrapping the solver with hard clauses self.oracle = Solver(name=solver_name, bootstrap_with=formula.hard, use_timer=use_timer) self.solver = solver_name # adding native cardinality constraints (if any) as hard clauses # this can be done only if the Minicard solver is in use if isinstance(formula, WCNFPlus) and formula.atms: assert self.oracle.supports_atmost(), \ '{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(solver_name) for atm in formula.atms: self.oracle.add_atmost(*atm) self.topv = formula.nv # top variable id self.soft = formula.soft self.sels = [] self.ucld = use_cld # mappings between internal and external variables VariableMap = collections.namedtuple('VariableMap', ['e2i', 'i2e']) self.vmap = VariableMap(e2i={}, i2e={}) # at this point internal and external variables are the same for v in range(1, formula.nv + 1): self.vmap.e2i[v] = v self.vmap.i2e[v] = v for cl in self.soft: sel = cl[0] if len(cl) > 1 or cl[0] < 0: self.topv += 1 sel = self.topv self.oracle.add_clause(cl + [-sel]) self.sels.append(sel) def __del__(self): """ Destructor. """ self.delete() def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def delete(self): """ Explicit destructor of the internal SAT oracle. """ if self.oracle: self.oracle.delete() self.oracle = None def add_clause(self, clause, soft=False): """ The method for adding a new hard of soft clause to the problem formula. Although the input formula is to be specified as an argument of the constructor of :class:`LBX`, adding clauses may be helpful when *enumerating* MCSes of the formula. This way, the clauses are added incrementally, i.e. *on the fly*. The clause to add can be any iterable over integer literals. The additional Boolean parameter ``soft`` can be set to ``True`` meaning the the clause being added is soft (note that parameter ``soft`` is set to ``False`` by default). Also note that besides pure clauses, the method can also expect native cardinality constraints represented as a pair ``(lits, bound)``. Only hard cardinality constraints can be added. :param clause: a clause to add :param soft: whether or not the clause is soft :type clause: iterable(int) :type soft: bool """ # first, map external literals to internal literals # introduce new variables if necessary cl = list( map( lambda l: self._map_extlit(l), clause if not len(clause) == 2 or not type(clause[0]) in (list, tuple, set) else clause[0])) if not soft: if not len(clause) == 2 or not type( clause[0]) in (list, tuple, set): # the clause is hard, and so we simply add it to the SAT oracle self.oracle.add_clause(cl) else: # this should be a native cardinality constraint, # which can be used only together with Minicard assert self.oracle.supports_atmost(), \ '{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver) self.oracle.add_atmost(cl, clause[1]) else: self.soft.append(cl) # soft clauses should be augmented with a selector sel = cl[0] if len(cl) > 1 or cl[0] < 0: self.topv += 1 sel = self.topv self.oracle.add_clause(cl + [-sel]) self.sels.append(sel) def compute(self, enable=[]): """ Compute and return one solution. This method checks whether the hard part of the formula is satisfiable, i.e. an MCS can be extracted. If the formula is satisfiable, the model computed by the SAT call is used as an *over-approximation* of the MCS in the method :func:`_compute` invoked here, which implements the LBX algorithm. An MCS is reported as a list of integers, each representing a soft clause index (the smallest index is ``1``). An optional input parameter is ``enable``, which represents a sequence (normally a list) of soft clause indices that a user would prefer to enable/satisfy. Note that this may result in an unsatisfiable oracle call, in which case ``None`` will be reported as solution. Also, the smallest clause index is assumed to be ``1``. :param enable: a sequence of clause ids to enable :type enable: iterable(int) :rtype: list(int) """ self.setd = [] self.satc = [False for cl in self.soft] # satisfied clauses self.solution = None self.bb_assumps = [] # backbone assumptions self.ss_assumps = [] # satisfied soft clause assumptions if self.oracle.solve( assumptions=[self.sels[cl_id - 1] for cl_id in enable]): # hard part is satisfiable => there is a solution self._filter_satisfied(update_setd=True) self._compute() self.solution = list( map(lambda i: i + 1, filter(lambda i: not self.satc[i], range(len(self.soft))))) return self.solution def enumerate(self): """ This method iterates through MCSes enumerating them until the formula has no more MCSes. The method iteratively invokes :func:`compute`. Note that the method does not block the MCSes computed - this should be explicitly done by a user. """ done = False while not done: mcs = self.compute() if mcs != None: yield mcs else: done = True def block(self, mcs): """ Block a (previously computed) MCS. The MCS should be given as an iterable of integers. Note that this method is not automatically invoked from :func:`enumerate` because a user may want to block some of the MCSes conditionally depending on the needs. For example, one may want to compute disjoint MCSes only in which case this standard blocking is not appropriate. :param mcs: an MCS to block :type mcs: iterable(int) """ self.oracle.add_clause([self.sels[cl_id - 1] for cl_id in mcs]) def _satisfied(self, cl, model): """ Given a clause (as an iterable of integers) and an assignment (as a list of integers), this method checks whether or not the assignment satisfies the clause. This is done by a simple clause traversal. The method is invoked from :func:`_filter_satisfied`. :param cl: a clause to check :param model: an assignment :type cl: iterable(int) :type model: list(int) :rtype: bool """ for l in cl: if len(model) < abs(l) or model[abs(l) - 1] == l: # either literal is unassigned or satisfied by the model return True return False def _filter_satisfied(self, update_setd=False): """ This method extracts a model provided by the previous call to a SAT oracle and iterates over all soft clauses checking if each of is satisfied by the model. Satisfied clauses are marked accordingly while the literals of the unsatisfied clauses are kept in a list called ``setd``, which is then used to refine the correction set (see :func:`_compute`, and :func:`do_cld_check`). Optional Boolean parameter ``update_setd`` enforces the method to update variable ``self.setd``. If this parameter is set to ``False``, the method only updates the list of satisfied clauses, which is an under-approximation of a *maximal satisfiable subset* (MSS). :param update_setd: whether or not to update setd :type update_setd: bool """ model = self.oracle.get_model() setd = set() for i, cl in enumerate(self.soft): if not self.satc[i]: if self._satisfied(cl, model): self.satc[i] = True self.ss_assumps.append(self.sels[i]) else: setd = setd.union(set(cl)) if update_setd: self.setd = sorted(setd) def _compute(self): """ The main method of the class, which computes an MCS given its over-approximation. The over-approximation is defined by a model for the hard part of the formula obtained in :func:`compute`. The method is essentially a simple loop going over all literals unsatisfied by the previous model, i.e. the literals of ``self.setd`` and checking which literals can be satisfied. This process can be seen a refinement of the over-approximation of the MCS. The algorithm follows the pseudo-code of the LBX algorithm presented in [1]_. Additionally, if :class:`LBX` was constructed with the requirement to make "clause :math:`D`" calls, the method calls :func:`do_cld_check` at every iteration of the loop using the literals of ``self.setd`` not yet checked, as the contents of "clause :math:`D`". """ # unless clause D checks are used, test one literal at a time # and add it either to satisfied of backbone assumptions i = 0 while i < len(self.setd): if self.ucld: self.do_cld_check(self.setd[i:]) i = 0 if self.setd: # if may be empty after the clause D check if self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps + [self.setd[i]]): # filtering satisfied clauses self._filter_satisfied() else: # current literal is backbone self.bb_assumps.append(-self.setd[i]) i += 1 def do_cld_check(self, cld): """ Do the "clause :math:`D`" check. This method receives a list of literals, which serves a "clause :math:`D`" [2]_, and checks whether the formula conjoined with :math:`D` is satisfiable. .. [2] Joao Marques-Silva, Federico Heras, Mikolas Janota, Alessandro Previti, Anton Belov. *On Computing Minimal Correction Subsets*. IJCAI 2013. pp. 615-622 If clause :math:`D` cannot be satisfied together with the formula, then negations of all of its literals are backbones of the formula and the LBX algorithm can stop. Otherwise, the literals satisfied by the new model refine the MCS further. Every time the method is called, a new fresh selector variable :math:`s` is introduced, which augments the current clause :math:`D`. The SAT oracle then checks if clause :math:`(D \\vee \\neg{s})` can be satisfied together with the internal formula. The :math:`D` clause is then disabled by adding a hard clause :math:`(\\neg{s})`. :param cld: clause :math:`D` to check :type cld: list(int) """ # adding a selector literal to clause D # selector literals for clauses D currently # cannot be reused, but this may change later self.topv += 1 sel = self.topv cld.append(-sel) # adding clause D self.oracle.add_clause(cld) if self.oracle.solve(assumptions=self.ss_assumps + self.bb_assumps + [sel]): # filtering satisfied self._filter_satisfied(update_setd=True) else: # clause D is unsatisfiable => all literals are backbones self.bb_assumps.extend([-l for l in cld[:-1]]) self.setd = [] # deactivating clause D self.oracle.add_clause([-sel]) def _map_extlit(self, l): """ Map an external variable to an internal one if necessary. This method is used when new clauses are added to the formula incrementally, which may result in introducing new variables clashing with the previously used *clause selectors*. The method makes sure no clash occurs, i.e. it maps the original variables used in the new problem clauses to the newly introduced auxiliary variables (see :func:`add_clause`). Given an integer literal, a fresh literal is returned. The returned integer has the same sign as the input literal. :param l: literal to map :type l: int :rtype: int """ v = abs(l) if v in self.vmap.e2i: return int(copysign(self.vmap.e2i[v], l)) else: self.topv += 1 self.vmap.e2i[v] = self.topv self.vmap.i2e[self.topv] = v return int(copysign(self.topv, l)) def oracle_time(self): """ Report the total SAT solving time. """ return self.oracle.time_accum()
class OptUx(object): """ A simple Python version of the implicit hitting set based optimal MUS extractor and enumerator. Given a (weighted) (partial) CNF formula, i.e. formula in the :class:`.WCNF` format, this class can be used to compute a given number of optimal MUS (starting from the *best* one) of the input formula. :class:`OptUx` roughly follows the implementation of Forqes [1]_ but lacks a few additional heuristics, which however aren't applied in Forqes by default. As a result, OptUx applies exhaustive *disjoint* minimal correction subset (MCS) enumeration [1]_, [2]_, [3]_, [4]_ with the incremental use of RC2 [5]_ as an underlying MaxSAT solver. Once disjoint MCSes are enumerated, they are used to bootstrap a hitting set solver. This implementation uses :class:`.Hitman` as a hitting set solver, which is again based on RC2. Note that in the main implicit hitting enumeration loop of the algorithm, OptUx follows Forqes in that it does not reduce correction subsets detected to minimal correction subsets. As a result, correction subsets computed in the main loop are added to :class:`Hitman` *unreduced*. :class:`OptUx` can use any SAT solver available in PySAT. The default SAT solver to use is ``g3``, which stands for Glucose 3 [6]_ (see :class:`.SolverNames`). Boolean parameters ``adapt``, ``exhaust``, and ``minz`` control whether or not the underlying :class:`.RC2` oracles should apply detection and adaptation of intrinsic AtMost1 constraints, core exhaustion, and core reduction. Also, unsatisfiable cores can be trimmed if the ``trim`` parameter is set to a non-zero integer. Finally, verbosity level can be set using the ``verbose`` parameter. .. [5] Alexey Ignatiev, Antonio Morgado, Joao Marques-Silva. *RC2: an Efficient MaxSAT Solver*. J. Satisf. Boolean Model. Comput. 11(1). 2019. pp. 53-64 .. [6] Gilles Audemard, Jean-Marie Lagniez, Laurent Simon. *Improving Glucose for Incremental SAT Solving with Assumptions: Application to MUS Extraction*. SAT 2013. pp. 309-317 :param formula: (weighted) (partial) CNF formula :param solver: SAT oracle name :param adapt: detect and adapt intrinsic AtMost1 constraints :param exhaust: do core exhaustion :param minz: do heuristic core reduction :param trim: do core trimming at most this number of times :param verbose: verbosity level :type formula: :class:`.WCNF` :type solver: str :type adapt: bool :type exhaust: bool :type minz: bool :type trim: int :type verbose: int """ def __init__(self, formula, solver='g3', adapt=False, exhaust=False, minz=False, trim=False, verbose=0): """ Constructor. """ # verbosity level self.verbose = verbose # constructing a local copy of the formula self.formula = WCNFPlus() self.formula.hard = formula.hard[:] self.formula.wght = formula.wght[:] self.formula.topw = formula.topw self.formula.nv = formula.nv # copying atmost constraints, if any if isinstance(formula, WCNFPlus) and formula.atms: self.formula.atms = formula.atms[:] # top variable identifier self.topv = formula.nv # processing soft clauses self._process_soft(formula) self.formula.nv = self.topv # creating an unweighted copy unweighted = self.formula.copy() unweighted.wght = [1 for w in unweighted.wght] # enumerating disjoint MCSes (including unit-size MCSes) to_hit, self.units = self._disjoint(unweighted, solver, adapt, exhaust, minz, trim) if self.verbose > 2: print('c mcses: {0} unit, {1} disj'.format( len(self.units), len(to_hit) + len(self.units))) # hitting set enumerator self.hitman = Hitman(bootstrap_with=to_hit, weights=self.weights, solver=solver, htype='sorted', mxs_adapt=adapt, mxs_exhaust=exhaust, mxs_minz=minz, mxs_trim=trim) # SAT oracle bootstrapped with the hard clauses; note that # clauses of the unit-size MCSes are enforced to be enabled self.oracle = Solver(name=solver, bootstrap_with=unweighted.hard + [[mcs] for mcs in self.units]) if unweighted.atms: assert self.oracle.supports_atmost(), \ '{0} does not support native cardinality constraints. Make sure you use the right type of formula.'.format(self.solver) for atm in unweighted.atms: self.oracle.add_atmost(*atm) def __del__(self): """ Destructor. """ self.delete() def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def delete(self): """ Explicit destructor of the internal hitting set and SAT oracles. """ if self.hitman: self.hitman.delete() self.hitman = None if self.oracle: self.oracle.delete() self.oracle = None def _process_soft(self, formula): """ The method is for processing the soft clauses of the input formula. Concretely, it checks which soft clauses must be relaxed by a unique selector literal and applies the relaxation. :param formula: input formula :type formula: :class:`.WCNF` """ # list of selectors self.sels = [] # mapping from selectors to clause ids self.smap = {} # duplicate unit clauses processed_dups = set() # processing the soft clauses for cl in formula.soft: # if the clause is unit-size, its sole literal acts a selector selv = cl[0] # if clause is not unit, we relax it if len(cl) > 1: self.topv += 1 selv = self.topv self.formula.hard.append(cl + [-selv]) elif selv in self.smap: # the clause is unit but a there is a previously seen # duplicate of this clause; this means we have to # reprocess the previous clause again and relax it if selv not in processed_dups: self.topv += 1 nsel = self.topv self.sels[self.smap[selv] - 1] = nsel self.formula.hard.append( self.formula.soft[self.smap[selv] - 1] + [-nsel]) self.formula.soft[self.smap[selv] - 1] = [nsel] self.smap[nsel] = self.smap[selv] processed_dups.add(selv) # processing the current clause self.topv += 1 selv = self.topv self.formula.hard.append(cl + [-selv]) self.sels.append(selv) self.formula.soft.append([selv]) self.smap[selv] = len(self.sels) # garbage-collecting the duplicates for selv in processed_dups: del self.smap[selv] # these numbers should be equal after the processing assert len(self.sels) == len(self.smap) == len(self.formula.wght) # creating a dictionary of weights self.weights = {l: w for l, w in zip(self.sels, self.formula.wght)} def _disjoint(self, formula, solver, adapt, exhaust, minz, trim): """ This method constitutes the preliminary step of the implicit hitting set paradigm of Forqes. Namely, it enumerates all the disjoint *minimal correction subsets* (MCSes) of the formula, which will be later used to bootstrap the hitting set solver. Note that the MaxSAT solver in use is :class:`.RC2`. As a result, all the input parameters of the method, namely, ``formula``, ``solver``, ``adapt``, `exhaust``, ``minz``, and ``trim`` - represent the input and the options for the RC2 solver. :param formula: input formula :param solver: SAT solver name :param adapt: detect and adapt AtMost1 constraints :param exhaust: exhaust unsatisfiable cores :param minz: apply heuristic core minimization :param trim: trim unsatisfiable cores at most this number of times :type formula: :class:`.WCNF` :type solver: str :type adapt: bool :type exhaust: bool :type minz: bool :type trim: int """ # these will store disjoint MCSes # (unit-size MCSes are stored separately) to_hit, units = [], [] with RC2(formula, solver=solver, adapt=adapt, exhaust=exhaust, minz=minz, trim=trim, verbose=0) as oracle: # iterating over MaxSAT solutions while True: # a new MaxSAT model model = oracle.compute() if model is None: # no model => no more disjoint MCSes break # extracting the MCS corresponding to the model falsified = list( filter(lambda l: model[abs(l) - 1] == -l, self.sels)) # unit size or not? if len(falsified) > 1: to_hit.append(falsified) else: units.append(falsified[0]) # blocking the MCS; # next time, all these clauses will be satisfied for l in falsified: oracle.add_clause([l]) # reporting the MCS if self.verbose > 3: print('c mcs: {0} 0'.format(' '.join( [str(self.smap[s]) for s in falsified]))) # RC2 will be destroyed next; let's keep the oracle time self.disj_time = oracle.oracle_time() return to_hit, units def compute(self): """ This method implements the main look of the implicit hitting set paradigm of Forqes to compute a best-cost MUS. The result MUS is returned as a list of integers, each representing a soft clause index. :rtype: list(int) """ # correctly computed cost of the unit-mcs component units_cost = sum( map(lambda l: self.weights[l], (l for l in self.units))) while True: # computing a new optimal hitting set hs = self.hitman.get() if hs is None: # no more hitting sets exist break # setting all the selector polarities to true self.oracle.set_phases(self.sels) # testing satisfiability of the {self.units + hs} subset res = self.oracle.solve(assumptions=hs) if res == False: # the candidate subset of clauses is unsatisfiable, # i.e. it is an optimal MUS we are searching for; # therefore, blocking it and returning self.hitman.block(hs) self.cost = self.hitman.oracle.cost + units_cost return sorted(map(lambda s: self.smap[s], self.units + hs)) else: # the candidate subset is satisfiable, # thus extracting a correction subset model = self.oracle.get_model() cs = list(filter(lambda l: model[abs(l) - 1] == -l, self.sels)) # hitting the new correction subset self.hitman.hit(cs, weights=self.weights) def enumerate(self): """ This is generator method iterating through MUSes and enumerating them until the formula has no more MUSes, or a user decides to stop the process. :rtype: list(int) """ done = False while not done: mus = self.compute() if mus != None: yield mus else: done = True def oracle_time(self): """ This method computes and returns the total SAT solving time involved. :rtype: float """ return self.disj_time + self.hitman.oracle_time( ) + self.oracle.time_accum()
class RC2(object): """ MaxSAT algorithm based on relaxable cardinality constraints (RC2/OLL). """ def __init__(self, formula, solver='g3', adapt=False, exhaust=False, incr=False, minz=False, trim=0, verbose=0): """ Constructor. """ # saving verbosity level and other options self.verbose = verbose self.exhaust = exhaust self.solver = solver self.adapt = adapt self.minz = minz self.trim = trim # clause selectors and mapping from selectors to clause ids self.sels, self.vmap = [], {} # other MaxSAT related stuff self.topv = self.orig_nv = formula.nv self.wght = {} # weights of soft clauses self.sums = [] # totalizer sum assumptions self.bnds = {} # a mapping from sum assumptions to totalizer bounds self.tobj = {} # a mapping from sum assumptions to totalizer objects self.cost = 0 # initialize SAT oracle with hard clauses only self.init(formula, incr=incr) # core minimization is going to be extremely expensive # for large plain formulas, and so we turn it off here wght = self.wght.values() if not formula.hard and len( self.sels) > 100000 and min(wght) == max(wght): self.minz = False def __enter__(self): """ 'with' constructor. """ return self def __exit__(self, exc_type, exc_value, traceback): """ 'with' destructor. """ self.delete() def init(self, formula, incr=False): """ Initialize the SAT solver. """ # creating a solver object self.oracle = Solver(name=self.solver, bootstrap_with=formula.hard, incr=incr, use_timer=True) # adding soft clauses to oracle for i, cl in enumerate(formula.soft): selv = cl[0] # if clause is unit, selector variable is its literal if len(cl) > 1: self.topv += 1 selv = self.topv cl.append(-self.topv) self.oracle.add_clause(cl) if selv not in self.wght: # record selector and its weight self.sels.append(selv) self.wght[selv] = formula.wght[i] self.vmap[selv] = i else: # selector is not new; increment its weight self.wght[selv] += formula.wght[i] self.sels_set = set(self.sels) if self.verbose > 1: print('c formula: {0} vars, {1} hard, {2} soft'.format( formula.nv, len(formula.hard), len(formula.soft))) def delete(self): """ Explicit destructor. """ if self.oracle: self.oracle.delete() self.oracle = None if self.solver != 'mc': # for minicard, there is nothing to free for t in six.itervalues(self.tobj): t.delete() def compute(self): """ Compute and return a solution. """ # simply apply MaxSAT only once res = self.compute_() if res: # extracting a model self.model = self.oracle.get_model() self.model = filter(lambda l: abs(l) <= self.orig_nv, self.model) return self.model def enumerate(self): """ Enumerate MaxSAT solutions (from best to worst). """ done = False while not done: model = self.compute() if model != None: self.oracle.add_clause([-l for l in model]) yield model else: done = True def compute_(self): """ Compute a MaxSAT solution with RC2. """ # trying to adapt (simplify) the formula # by detecting and using atmost1 constraints if self.adapt: self.adapt_am1() # main solving loop while not self.oracle.solve(assumptions=self.sels + self.sums): self.get_core() if not self.core: # core is empty, i.e. hard part is unsatisfiable return False self.process_core() if self.verbose > 1: print('c cost: {0}; core sz: {1}; soft sz: {2}'.format( self.cost, len(self.core), len(self.sels) + len(self.sums))) return True def get_core(self): """ Extract unsatisfiable core. """ # extracting the core self.core = self.oracle.get_core() if self.core: # try to reduce the core by trimming self.trim_core() # and by heuristic minimization self.minimize_core() # core weight self.minw = min(map(lambda l: self.wght[l], self.core)) # dividing the core into two parts iter1, iter2 = itertools.tee(self.core) self.core_sels = list(l for l in iter1 if l in self.sels_set) self.core_sums = list(l for l in iter2 if l not in self.sels_set) def process_core(self): """ Deal with a core found in the main loop. """ # updating the cost self.cost += self.minw # assumptions to remove self.garbage = set() if len(self.core_sels) != 1 or len(self.core_sums) > 0: # process selectors in the core self.process_sels() # process previously introducded sums in the core self.process_sums() if len(self.rels) > 1: # create a new cardunality constraint t = self.create_sum() # apply core exhaustion if required b = self.exhaust_core(t) if self.exhaust else 1 if b: # save the info about this sum and # add its assumption literal self.set_bound(t, b) else: # impossible to satisfy any of these clauses # they must become hard for relv in self.rels: self.oracle.add_clause([relv]) else: # unit cores are treated differently # (their negation is added to the hard part) self.oracle.add_clause([-self.core_sels[0]]) self.garbage.add(self.core_sels[0]) # remove unnecessary assumptions self.filter_assumps() def adapt_am1(self): """ Try to detect atmost1 constraints involving soft literals. """ # literal connections conns = collections.defaultdict(lambda: set([])) confl = [] # prepare connections for l1 in self.sels: st, props = self.oracle.propagate(assumptions=[l1], phase_saving=2) if st: for l2 in props: if -l2 in self.sels_set: conns[l1].add(-l2) conns[-l2].add(l1) else: # propagating this literal results in a conflict confl.append(l1) if confl: # filtering out unnecessary connections ccopy = {} confl = set(confl) for l in conns: if l not in confl: cc = conns[l].difference(confl) if cc: ccopy[l] = cc conns = ccopy confl = list(confl) # processing unit size cores for l in confl: self.core, self.minw = [l], self.wght[l] self.core_sels, self.core_sums = [l], [] self.process_core() if self.verbose > 1: print('c unit cores found: {0}; cost: {1}'.format( len(confl), self.cost)) nof_am1 = 0 len_am1 = [] lits = set(conns.keys()) while lits: am1 = [min(lits, key=lambda l: len(conns[l]))] for l in sorted(conns[am1[0]], key=lambda l: len(conns[l])): if l in lits: for l_added in am1[1:]: if l_added not in conns[l]: break else: am1.append(l) # updating remaining lits and connections lits.difference_update(set(am1)) for l in conns: conns[l] = conns[l].difference(set(am1)) if len(am1) > 1: # treat the new atmost1 relation self.process_am1(am1) nof_am1 += 1 len_am1.append(len(am1)) # updating the set of selectors self.sels_set = set(self.sels) if self.verbose > 1 and nof_am1: print('c am1s found: {0}; avgsz: {1:.1f}; cost: {2}'.format( nof_am1, sum(len_am1) / float(nof_am1), self.cost)) def process_am1(self, am1): """ Process an atmost1 relation detected (treat as a core). """ # computing am1's weight self.minw = min(map(lambda l: self.wght[l], am1)) # pretending am1 to be a core, and the bound is its size - 1 self.core_sels, b = am1, len(am1) - 1 # incrementing the cost self.cost += b * self.minw # assumptions to remove self.garbage = set() # splitting and relaxing if needed self.process_sels() # new selector self.topv += 1 selv = self.topv self.oracle.add_clause([-l for l in self.rels] + [-selv]) # integrating the new selector self.sels.append(selv) self.wght[selv] = self.minw self.vmap[selv] = len(self.wght) - 1 # removing unnecessary assumptions self.filter_assumps() def trim_core(self): """ Trim unsatisfiable core at most a given number of times. """ for i in range(self.trim): # call solver with core assumption only # it must return 'unsatisfiable' self.oracle.solve(assumptions=self.core) # extract a new core new_core = self.oracle.get_core() if len(new_core) == len(self.core): # stop if new core is not better than the previous one break # otherwise, update core self.core = new_core def minimize_core(self): """ Try to minimize a core and compute an approximation of an MUS. Simple deletion-based MUS extraction. """ if self.minz and len(self.core) > 1: self.core = sorted(self.core, key=lambda l: self.wght[l]) self.oracle.conf_budget(1000) i = 0 while i < len(self.core): to_test = self.core[:i] + self.core[(i + 1):] if self.oracle.solve_limited(assumptions=to_test) == False: self.core = to_test else: i += 1 def exhaust_core(self, tobj): """ Exhaust core by increasing its bound as much as possible. """ # the first case is simpler if self.oracle.solve(assumptions=[-tobj.rhs[1]]): return 1 else: self.cost += self.minw for i in range(2, len(self.rels)): # saving the previous bound self.tobj[-tobj.rhs[i - 1]] = tobj self.bnds[-tobj.rhs[i - 1]] = i - 1 # increasing the bound self.update_sum(-tobj.rhs[i - 1]) if self.oracle.solve(assumptions=[-tobj.rhs[i]]): # the bound should be equal to i return i # the cost should increase further self.cost += self.minw return None def process_sels(self): """ Process soft clause selectors participating in a new core. """ # new relaxation variables self.rels = [] for l in self.core_sels: if self.wght[l] == self.minw: # marking variable as being a part of the core # so that next time it is not used as an assump self.garbage.add(l) # reuse assumption variable as relaxation self.rels.append(-l) else: # do not remove this variable from assumps # since it has a remaining non-zero weight self.wght[l] -= self.minw # it is an unrelaxed soft clause, # a new relaxed copy of which we add to the solver self.topv += 1 self.oracle.add_clause([l, self.topv]) self.rels.append(self.topv) def process_sums(self): """ Process cardinality sums participating in a new core. """ for l in self.core_sums: if self.wght[l] == self.minw: # marking variable as being a part of the core # so that next time it is not used as an assump self.garbage.add(l) else: # do not remove this variable from assumps # since it has a remaining non-zero weight self.wght[l] -= self.minw # increase bound for the sum t, b = self.update_sum(l) # updating bounds and weights if b < len(t.rhs): lnew = -t.rhs[b] if lnew in self.garbage: self.garbage.remove(lnew) self.wght[lnew] = 0 if lnew not in self.wght: self.set_bound(t, b) else: self.wght[lnew] += self.minw # put this assumption to relaxation vars self.rels.append(-l) def create_sum(self, bound=1): """ Create a totalizer object encoding a new cardinality constraint. For Minicard, native atmostb constraints is used instead. """ if self.solver != 'mc': # standard totalizer-based encoding # new totalizer sum t = ITotalizer(lits=self.rels, ubound=bound, top_id=self.topv) # updating top variable id self.topv = t.top_id # adding its clauses to oracle for cl in t.cnf.clauses: self.oracle.add_clause(cl) else: # for minicard, use native cardinality constraints instead of the # standard totalizer, i.e. create a new (empty) totalizer sum and # fill it with the necessary data supported by minicard t = ITotalizer() t.lits = self.rels self.topv += 1 # a new variable will represent the bound # proper initial bound t.rhs = [None] * (len(t.lits)) t.rhs[bound] = self.topv # new atmostb constraint instrumented with # an implication and represented natively rhs = len(t.lits) amb = [[-self.topv] * (rhs - bound) + t.lits, rhs] # add constraint to the solver self.oracle.add_atmost(*amb) return t def update_sum(self, assump): """ Increase the bound for a given totalizer object. """ # getting a totalizer object corresponding to assumption t = self.tobj[assump] # increment the current bound b = self.bnds[assump] + 1 if self.solver != 'mc': # the case of standard totalizer encoding # increasing its bound t.increase(ubound=b, top_id=self.topv) # updating top variable id self.topv = t.top_id # adding its clauses to oracle if t.nof_new: for cl in t.cnf.clauses[-t.nof_new:]: self.oracle.add_clause(cl) else: # the case of cardinality constraints represented natively # right-hand side is always equal to the number of input literals rhs = len(t.lits) if b < rhs: # creating an additional bound if not t.rhs[b]: self.topv += 1 t.rhs[b] = self.topv # a new at-most-b constraint amb = [[-t.rhs[b]] * (rhs - b) + t.lits, rhs] self.oracle.add_atmost(*amb) return t, b def set_bound(self, tobj, rhs): """ Set a bound for a given totalizer object. """ # saving the sum and its weight in a mapping self.tobj[-tobj.rhs[rhs]] = tobj self.bnds[-tobj.rhs[rhs]] = rhs self.wght[-tobj.rhs[rhs]] = self.minw # adding a new assumption to force the sum to be at most rhs self.sums.append(-tobj.rhs[rhs]) def filter_assumps(self): """ Filter out both unnecessary selectors and sums. """ self.sels = list(filter(lambda x: x not in self.garbage, self.sels)) self.sums = list(filter(lambda x: x not in self.garbage, self.sums)) self.bnds = { l: b for l, b in six.iteritems(self.bnds) if l not in self.garbage } self.wght = { l: w for l, w in six.iteritems(self.wght) if l not in self.garbage } self.garbage.clear() def oracle_time(self): """ Report the total SAT solving time. """ return self.oracle.time_accum()