示例#1
0
文件: mcsls.py 项目: vedadux/pysat
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()
示例#2
0
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
示例#3
0
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()
示例#4
0
文件: fm.py 项目: sschnug/pysat
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
示例#5
0
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()
示例#6
0
文件: optux.py 项目: pysathq/pysat
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()
示例#7
0
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()