def init(self, bootstrap_with): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. :param bootstrap_with: input set of sets to hit :type bootstrap_with: iterable(iterable(obj)) """ # formula encoding the sets to hit formula = WCNF() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): formula.append([-obj_id], weight=1) if self.htype == 'rc2': # using the RC2-A options from MaxSAT evaluation 2018 self.oracle = RC2(formula, solver=self.solver, adapt=False, exhaust=True, trim=5) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=True) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=True)
def init(self, bootstrap_with, weights=None): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. An additional optional parameter is ``weights``, which can be used to specify non-unit weights for the target objects in the sets to hit. This only works if ``'sorted'`` enumeration of hitting sets is applied. :param bootstrap_with: input set of sets to hit :param weights: weights of the objects in case the problem is weighted :type bootstrap_with: iterable(iterable(obj)) :type weights: dict(obj) """ # formula encoding the sets to hit formula = WCNF() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): formula.append( [-obj_id], weight=1 if not weights else weights[self.idpool.obj(obj_id)]) if self.htype == 'rc2': if not weights or min(weights.values()) == max(weights.values()): self.oracle = RC2(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, trim=self.trim) else: self.oracle = RC2Stratified(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, nohard=True, trim=self.trim) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=self.usecld) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=self.usecld)
def init(self, bootstrap_with, costs): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. :param bootstrap_with: input set of sets to hit :type bootstrap_with: iterable(iterable(obj)) """ # formula encoding the sets to hit formula = WCNF() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): # this is saying that not including a clause is given a weight of x # maxSAT is MAXIMISING the sum of satisfied soft clauses, so to minimise sum, # we want to weight *not* including something (hence the -obj_id) # this means words such as <PAD> should be given a *higher* weight, so the # solver decides that NOT including <PAD> is more worth it than not including # a more "meaningful" word cost = costs[obj_id - 1] formula.append([-obj_id], weight=cost) if self.htype == 'rc2': # using the RC2-A options from MaxSAT evaluation 2018 self.oracle = RC2(formula, solver=self.solver, adapt=False, exhaust=True, trim=5) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=True) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=True)
class Hitman(object): """ A cardinality-/subset-minimal hitting set enumerator. The enumerator can be set up to use either a MaxSAT solver :class:`.RC2` or an MCS enumerator (either :class:`.LBX` or :class:`.MCSls`). In the former case, the hitting sets enumerated are ordered by size (smallest size hitting sets are computed first), i.e. *sorted*. In the latter case, subset-minimal hitting are enumerated in an arbitrary order, i.e. *unsorted*. This is handled with the use of parameter ``htype``, which is set to be ``'sorted'`` by default. The MaxSAT-based enumerator can be chosen by setting ``htype`` to one of the following values: ``'maxsat'``, ``'mxsat'``, or ``'rc2'``. Alternatively, by setting it to ``'mcs'`` or ``'lbx'``, a user can enforce using the :class:`.LBX` MCS enumerator. If ``htype`` is set to ``'mcsls'``, the :class:`.MCSls` enumerator is used. In either case, an underlying problem solver can use a SAT oracle specified as an input parameter ``solver``. The default SAT solver is Glucose3 (specified as ``g3``, see :class:`.SolverNames` for details). Objects of class :class:`Hitman` can be bootstrapped with an iterable of iterables, e.g. a list of lists. This is handled using the ``bootstrap_with`` parameter. Each set to hit can comprise elements of any type, e.g. integers, strings or objects of any Python class, as well as their combinations. The bootstrapping phase is done in :func:`init`. A few other optional parameters include the possible options for RC2 as well as for LBX- and MCSls-like MCS enumerators that control the behaviour of the underlying solvers. :param bootstrap_with: input set of sets to hit :param weights: a mapping from objects to their weights (if weighted) :param solver: name of SAT solver :param htype: enumerator type :param mxs_adapt: detect and process AtMost1 constraints in RC2 :param mxs_exhaust: apply unsatisfiable core exhaustion in RC2 :param mxs_minz: apply heuristic core minimization in RC2 :param mxs_trim: trim unsatisfiable cores at most this number of times :param mcs_usecld: use clause-D heuristic in the MCS enumerator :type bootstrap_with: iterable(iterable(obj)) :type weights: dict(obj) :type solver: str :type htype: str :type mxs_adapt: bool :type mxs_exhaust: bool :type mxs_minz: bool :type mxs_trim: int :type mcs_usecld: bool """ def __init__(self, bootstrap_with=[], weights=None, solver='g3', htype='sorted', mxs_adapt=False, mxs_exhaust=False, mxs_minz=False, mxs_trim=0, mcs_usecld=False): """ Constructor. """ # hitting set solver self.oracle = None # name of SAT solver self.solver = solver # various oracle options self.adapt = mxs_adapt self.exhaust = mxs_exhaust self.minz = mxs_minz self.trim = mxs_trim self.usecld = mcs_usecld # hitman type: either a MaxSAT solver or an MCS enumerator if htype in ('maxsat', 'mxsat', 'rc2', 'sorted'): self.htype = 'rc2' elif htype in ('mcs', 'lbx'): self.htype = 'lbx' else: # 'mcsls' self.htype = 'mcsls' # pool of variable identifiers (for objects to hit) self.idpool = IDPool() # initialize hitting set solver self.init(bootstrap_with, weights) 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 init(self, bootstrap_with, weights=None): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. An additional optional parameter is ``weights``, which can be used to specify non-unit weights for the target objects in the sets to hit. This only works if ``'sorted'`` enumeration of hitting sets is applied. :param bootstrap_with: input set of sets to hit :param weights: weights of the objects in case the problem is weighted :type bootstrap_with: iterable(iterable(obj)) :type weights: dict(obj) """ # formula encoding the sets to hit formula = WCNF() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): formula.append( [-obj_id], weight=1 if not weights else weights[self.idpool.obj(obj_id)]) if self.htype == 'rc2': if not weights or min(weights.values()) == max(weights.values()): self.oracle = RC2(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, trim=self.trim) else: self.oracle = RC2Stratified(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, nohard=True, trim=self.trim) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=self.usecld) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=self.usecld) def delete(self): """ Explicit destructor of the internal hitting set oracle. """ if self.oracle: self.oracle.delete() self.oracle = None def get(self): """ This method computes and returns a hitting set. The hitting set is obtained using the underlying oracle operating the MaxSAT problem formulation. The computed solution is mapped back to objects of the problem domain. :rtype: list(obj) """ model = self.oracle.compute() if model is not None: if self.htype == 'rc2': # extracting a hitting set self.hset = filter(lambda v: v > 0, model) else: self.hset = model return list(map(lambda vid: self.idpool.id2obj[vid], self.hset)) def hit(self, to_hit, weights=None): """ This method adds a new set to hit to the hitting set solver. This is done by translating the input iterable of objects into a list of Boolean variables in the MaxSAT problem formulation. Note that an optional parameter that can be passed to this method is ``weights``, which contains a mapping the objects under question into weights. Also note that the weight of an object must not change from one call of :meth:`hit` to another. :param to_hit: a new set to hit :param weights: a mapping from objects to weights :type to_hit: iterable(obj) :type weights: dict(obj) """ # translating objects to variables to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) # a soft clause should be added for each new object new_obj = list( filter(lambda vid: vid not in self.oracle.vmap.e2i, to_hit)) # new hard clause self.oracle.add_clause(to_hit) # new soft clauses for vid in new_obj: self.oracle.add_clause( [-vid], 1 if not weights else weights[self.idpool.obj(vid)]) def block(self, to_block, weights=None): """ The method serves for imposing a constraint forbidding the hitting set solver to compute a given hitting set. Each set to block is encoded as a hard clause in the MaxSAT problem formulation, which is then added to the underlying oracle. Note that an optional parameter that can be passed to this method is ``weights``, which contains a mapping the objects under question into weights. Also note that the weight of an object must not change from one call of :meth:`hit` to another. :param to_block: a set to block :param weights: a mapping from objects to weights :type to_block: iterable(obj) :type weights: dict(obj) """ # translating objects to variables to_block = list(map(lambda obj: self.idpool.id(obj), to_block)) # a soft clause should be added for each new object new_obj = list( filter(lambda vid: vid not in self.oracle.vmap.e2i, to_block)) # new hard clause self.oracle.add_clause([-vid for vid in to_block]) # new soft clauses for vid in new_obj: self.oracle.add_clause( [-vid], 1 if not weights else weights[self.idpool.obj(vid)]) def enumerate(self): """ The method can be used as a simple iterator computing and blocking the hitting sets on the fly. It essentially calls :func:`get` followed by :func:`block`. Each hitting set is reported as a list of objects in the original problem domain, i.e. it is mapped back from the solutions over Boolean variables computed by the underlying oracle. :rtype: list(obj) """ done = False while not done: hset = self.get() if hset != None: self.block(hset) yield hset else: done = True def oracle_time(self): """ Report the total SAT solving time. """ return self.oracle.oracle_time()
class Hitman(object): """ A cardinality-/subset-minimal hitting set enumerator. The enumerator can be set up to use either a MaxSAT solver :class:`.RC2` or an MCS enumerator (either :class:`.LBX` or :class:`.MCSls`). In the former case, the hitting sets enumerated are ordered by size (smallest size hitting sets are computed first), i.e. *sorted*. In the latter case, subset-minimal hitting are enumerated in an arbitrary order, i.e. *unsorted*. This is handled with the use of parameter ``htype``, which is set to be ``'sorted'`` by default. The MaxSAT-based enumerator can be chosen by setting ``htype`` to one of the following values: ``'maxsat'``, ``'mxsat'``, or ``'rc2'``. Alternatively, by setting it to ``'mcs'`` or ``'lbx'``, a user can enforce using the :class:`.LBX` MCS enumerator. If ``htype`` is set to ``'mcsls'``, the :class:`.MCSls` enumerator is used. In either case, an underlying problem solver can use a SAT oracle specified as an input parameter ``solver``. The default SAT solver is Glucose3 (specified as ``g3``, see :class:`.SolverNames` for details). Objects of class :class:`Hitman` can be bootstrapped with an iterable of iterables, e.g. a list of lists. This is handled using the ``bootstrap_with`` parameter. Each set to hit can comprise elements of any type, e.g. integers, strings or objects of any Python class, as well as their combinations. The bootstrapping phase is done in :func:`init`. :param bootstrap_with: input set of sets to hit :param solver: name of SAT solver :param htype: enumerator type :type bootstrap_with: iterable(iterable(obj)) :type solver: str :type htype: str """ def __init__(self, bootstrap_with=[], solver='g3', htype='sorted'): """ Constructor. """ # hitting set solver self.oracle = None # name of SAT solver self.solver = solver # hitman type: either a MaxSAT solver or an MCS enumerator if htype in ('maxsat', 'mxsat', 'rc2', 'sorted'): self.htype = 'rc2' elif htype in ('mcs', 'lbx'): self.htype = 'lbx' else: # 'mcsls' self.htype = 'mcsls' # pool of variable identifiers (for objects to hit) self.idpool = IDPool() # initialize hitting set solver self.init(bootstrap_with) 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 init(self, bootstrap_with): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. :param bootstrap_with: input set of sets to hit :type bootstrap_with: iterable(iterable(obj)) """ # formula encoding the sets to hit formula = WCNF() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): formula.append([-obj_id], weight=1) if self.htype == 'rc2': # using the RC2-A options from MaxSAT evaluation 2018 self.oracle = RC2(formula, solver=self.solver, adapt=False, exhaust=True, trim=5) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=True) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=True) def delete(self): """ Explicit destructor of the internal hitting set oracle. """ if self.oracle: self.oracle.delete() self.oracle = None def get(self): """ This method computes and returns a hitting set. The hitting set is obtained using the underlying oracle operating the MaxSAT problem formulation. The computed solution is mapped back to objects of the problem domain. :rtype: list(obj) """ model = self.oracle.compute() if model: if self.htype == 'rc2': # extracting a hitting set self.hset = filter(lambda v: v > 0, model) else: self.hset = model return list(map(lambda vid: self.idpool.id2obj[vid], self.hset)) def hit(self, to_hit): """ This method adds a new set to hit to the hitting set solver. This is done by translating the input iterable of objects into a list of Boolean variables in the MaxSAT problem formulation. :param to_hit: a new set to hit :type to_hit: iterable(obj) """ # translating objects to variables to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) # a soft clause should be added for each new object new_obj = filter(lambda vid: vid not in self.oracle.vmap.e2i, to_hit) # new hard clause self.oracle.add_clause(to_hit) # new soft clauses for vid in new_obj: self.oracle.add_clause([-vid], 1) def block(self, to_block): """ The method serves for imposing a constraint forbidding the hitting set solver to compute a given hitting set. Each set to block is encoded as a hard clause in the MaxSAT problem formulation, which is then added to the underlying oracle. :param to_block: a set to block :type to_block: iterable(obj) """ # translating objects to variables to_block = list(map(lambda obj: self.idpool.id(obj), to_block)) # a soft clause should be added for each new object new_obj = filter(lambda vid: vid not in self.oracle.vmap.e2i, to_block) # new hard clause self.oracle.add_clause([-vid for vid in to_block]) # new soft clauses for vid in new_obj: self.oracle.add_clause([-vid], 1) def enumerate(self): """ The method can be used as a simple iterator computing and blocking the hitting sets on the fly. It essentially calls :func:`get` followed by :func:`block`. Each hitting set is reported as a list of objects in the original problem domain, i.e. it is mapped back from the solutions over Boolean variables computed by the underlying oracle. :rtype: list(obj) """ done = False while not done: hset = self.get() if hset != None: self.block(hset) yield hset else: done = True
def init(self, bootstrap_with, weights=None, subject_to=[]): """ This method serves for initializing the hitting set solver with a given list of sets to hit. Concretely, the hitting set problem is encoded into partial MaxSAT as outlined above, which is then fed either to a MaxSAT solver or an MCS enumerator. An additional optional parameter is ``weights``, which can be used to specify non-unit weights for the target objects in the sets to hit. This only works if ``'sorted'`` enumeration of hitting sets is applied. Another optional parameter is available, namely, ``subject_to``. It can be used to specify arbitrary hard constraints that must be respected when computing hitting sets of the given sets. Note that ``subject_to`` should be an iterable containing pure clauses and/or native AtMostK constraints. Finally, note that these hard constraints must be defined over the set of signed atomic objects, i.e. instances of class :class:`.Atom`. :param bootstrap_with: input set of sets to hit :param weights: weights of the objects in case the problem is weighted :param subject_to: hard constraints (either clauses or native AtMostK constraints) :type bootstrap_with: iterable(iterable(obj)) :type weights: dict(obj) :type subject_to: iterable(iterable(Atom)) """ # formula encoding the sets to hit formula = WCNFPlus() # hard clauses for to_hit in bootstrap_with: to_hit = list(map(lambda obj: self.idpool.id(obj), to_hit)) formula.append(to_hit) # additional hard constraints for cl in subject_to: if not len(cl) == 2 or not type(cl[0]) in (list, tuple, set): # this is a pure clause formula.append(list(map(lambda a: self.idpool.id(a.obj) * (2 * a.sign - 1), cl))) else: # this is a native AtMostK constraint formula.append([list(map(lambda a: self.idpool.id(a.obj) * (2 * a.sign - 1), cl[0])), cl[1]], is_atmost=True) # soft clauses for obj_id in six.iterkeys(self.idpool.id2obj): formula.append([-obj_id], weight=1 if not weights else weights[self.idpool.obj(obj_id)]) if self.htype == 'rc2': if not weights or min(weights.values()) == max(weights.values()): self.oracle = RC2(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, trim=self.trim) else: self.oracle = RC2Stratified(formula, solver=self.solver, adapt=self.adapt, exhaust=self.exhaust, minz=self.minz, nohard=True, trim=self.trim) elif self.htype == 'lbx': self.oracle = LBX(formula, solver_name=self.solver, use_cld=self.usecld) else: self.oracle = MCSls(formula, solver_name=self.solver, use_cld=self.usecld)