def get_constraint(idpool: IDPool, id2varmap, constraint: tConstraint) -> CNFPlus: """ Generate formula for a given cardinality constraint""" validate_constraint(constraint) lits = [] for ta in constraint.tas: t1 = tuple((constraint.course_name, ta)) if t1 not in id2varmap.keys(): id1 = idpool.id(t1) id2varmap[t1] = id1 else: id1 = id2varmap[t1] lits.append(id1) if constraint.type == tCardType.GREATEROREQUALS: if (constraint.bound == 1): cnf = CNFPlus() cnf.append(lits) elif (constraint.bound > len(lits)): msg = "Num TAs available for constraint:" + constraint.con_str + "is more than the bound in the constraint. \ Changing the bound to " + str(len(lits)) + ".\n" print(msg, file=sys.stderr) constraint.bound = len(lits) cnf = CardEnc.atleast(lits, vpool=idpool, bound=constraint.bound) elif constraint.type == tCardType.LESSOREQUALS: cnf = CardEnc.atmost(lits, vpool=idpool, bound=constraint.bound) return cnf
def to_cnf(self, x_vars, cnf): d_vars = [[cnf.nv + 1 + j + (j * i) for j in range(self.out_dim)] for i in range(self.out_dim)] cnf.nv += self.out_dim**2 for j in range(self.out_dim): for i in range(self.out_dim): E_ij = np.ceil((self.b[i] - self.b[j] + np.sum(self.A[i]) - np.sum(self.A[j])) / 2) lits = [ x if (ai > 0 and aj < 0) else -x for (x, ai, aj) in zip(x_vars, self.A[i], self.A[j]) if not (np.sign(ai) == np.sign(aj)) ] ale = CardEnc.atleast(lits, np.ceil(E_ij / 2.0), cnf.nv, EncType.seqcounter) r = ale.nv # TODO: Check assumption that this is "r(n, D)" of AAAI paper cnf.extend(ale.clauses) cnf.append([-r, d_vars[i][j]]) cnf.append([r, -d_vars[i][j]]) return d_vars
def uniqueness_clauses(self, row, col): clauses = [] for turn in range(2, self.num_turns): lits = [self.vpool.id((turn, row, col, state)) for state in STATES] clauses.extend( CardEnc.equals(lits, bound=1, vpool=self.vpool).clauses) return clauses
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 dominating_subset(self, k=1): """ Check if there exists a vertex cover of, at most, k-vertices. Accepts as params: - n_color: number of color to check - verbose: whether or not print the process """ if not self.edges(): return [] logging.info('\nCodifying SAT Solver...') solver = Solver(name='cd') vpool = IDPool() vertices_ids = [vpool.id(vertex) for vertex in self.vertices()] logging.info(' -> Codifying: Every vertex must be accessible') for vertex in self.vertices(): solver.add_clause([vpool.id(vertex)] + [ vpool.id(adjacent_vertex) for adjacent_vertex in self[vertex] ]) logging.info(' -> Codifying: At most', k, 'vertices should be selected') cnf = CardEnc.atmost(lits=vertices_ids, bound=k, vpool=vpool) solver.append_formula(cnf) logging.info('Running SAT Solver...') return solver.solve()
def prepare_hitman(pixels, inputs, intervals, htype): """ Initialize a hitting set enumerator. """ if not pixels: pixels = sorted(range(len(inputs))) # new Hitman object h = Hitman(htype=htype) # first variables should be related with the elements of the sets to hit # that is why we are adding soft clauses first for p in pixels: for v in range(intervals): var = h.idpool.id(tuple([inputs[p], v])) h.oracle.add_clause([-var], 1) # at most one value per pixel can be selected for p in pixels: lits = [h.idpool.id(tuple([inputs[p], v])) for v in range(intervals)] cnf = CardEnc.atmost(lits, encoding=EncType.pairwise) for cl in cnf.clauses: h.oracle.add_clause(cl) return h
def generate_medic_clauses(self): clauses = [] for turn in range(self.num_turns): lits = [ self.vpool.id((turn, row, col, IMMUNE_RECENTLY)) for row in range(self.height) for col in range(self.width) ] clauses.extend( CardEnc.atmost(lits, bound=self.num_medics, vpool=self.vpool).clauses) # TODO check case of 0 medics if self.num_medics == 0: return clauses for turn in range(self.num_turns - 1): for num_healthy in range(self.width * self.height): for healthy_tiles in itertools.combinations( self.tiles, num_healthy): sick_tiles = [ tile for tile in self.tiles if tile not in healthy_tiles ] clause = [] for row, col in healthy_tiles: clause.append(-self.vpool.id((turn, row, col, HEALTHY))) for row, col in sick_tiles: clause.append(self.vpool.id((turn, row, col, HEALTHY))) lits = [ self.vpool.id((turn + 1, row, col, IMMUNE_RECENTLY)) for row, col in healthy_tiles ] equals_clauses = CardEnc.equals(lits, bound=min( self.num_medics, num_healthy), vpool=self.vpool).clauses for sub_clause in equals_clauses: temp_clause = deepcopy(clause) temp_clause += sub_clause clauses.append(temp_clause) return clauses
def second_turn_clauses(self, row, col): lits = [ self.vpool.id((1, row, col, state)) for state in SECOND_TURN_STATES ] clauses = CardEnc.equals(lits, bound=1, vpool=self.vpool).clauses for state in STATES: if state not in SECOND_TURN_STATES: clauses.append([-self.vpool.id((0, row, col, state))]) return clauses
def hadar_dynamics(self): clauses = [] for t in range(self.num_observations): clauses.extend( CardEnc.atmost(self.__get_I0_predicates(t), bound=self.medics, vpool=self.pool).clauses) if self.medics == 0: return clauses for t in range(self.num_observations - 1): for num_healthy in range(self.cols * self.rows): for healthy_tiles in itertools.combinations( self.tiles, num_healthy): sick_tiles = [ tile for tile in self.tiles if tile not in healthy_tiles ] clause = [] for i, j in healthy_tiles: clause.append(-self.obj2id[f"H_{i}_{j}^{t}"]) for i, j in sick_tiles: clause.append(self.obj2id[f"H_{i}_{j}^{t}"]) lits = [ self.obj2id[f"I0_{i}_{j}^{t + 1}"] for i, j in healthy_tiles ] equals_clauses = CardEnc.equals(lits, bound=min( self.medics, num_healthy), vpool=self.pool).clauses for sub_clause in equals_clauses: temp_clause = copy.deepcopy(clause) temp_clause += sub_clause clauses.append(temp_clause) return CNF(from_clauses=clauses)
def first_step(self, first_field=None): self.solver.append_formula( CardEnc.equals(lits=list(range(1, self.game.board_dim**2 + 1)), bound=self.game.num_mines, encoding=EncType.native)) if first_field: row, column = first_field self.newly_opened = self.open(self.game.board[row][column]) else: self.newly_opened = self.open(self.get_random_field())
def init_literal_meaning(self): counter = 1 states = ["S2", "S1", "S0", "Q1", "Q0", "U", "I0", "I1", "H"] for turn, turn_state in enumerate(self.map): self.literal_meanings[turn] = {} for i, row in enumerate(turn_state): for j, item in enumerate(row): for num in range(len(states)): self.rev_literal_meanings[counter + num] = (turn, (i, j), states[num]) self.literal_meanings[turn][(i, j)] = { state: counter + num for num, state in enumerate(states) } cnf = CardEnc.equals(lits=list( self.literal_meanings[turn][(i, j)].values()), bound=1, encoding=EncType.pairwise) for clause in cnf.clauses: self.solver.add_clause(clause) counter += len(states) self.literal_meanings[turn][(i, j)]["D"] = counter self.rev_literal_meanings[counter] = (turn, (i, j), "D") counter += 1 self.literal_meanings[turn][(i, j)]["P"] = counter self.rev_literal_meanings[counter] = (turn, (i, j), "P") counter += 1 self.literal_meanings[turn][(i, j)]["Sick"] = counter self.rev_literal_meanings[counter] = (turn, (i, j), "Sick") counter += 1 not_s = ["Q1", "Q0", "U", "I0", "I1", "H", "Sick"] literals = [ self.literal_meanings[turn][(i, j)][state] for state in not_s ] cnf = CardEnc.equals(lits=literals, bound=1, encoding=EncType.pairwise) for clause in cnf.clauses: self.solver.add_clause(clause)
def test_atmostk(): for l in range(5, 10): for k in range(2, l): for e in encs: cnf = CardEnc.atmost(lits=list(range(1, l + 1)), bound=k, encoding=getattr(EncType, e)) # enumerating all models with MinisatGH(bootstrap_with=cnf) as solver: for num, model in enumerate(solver.enum_models(), 1): solver.add_clause([-l for l in model[:l]]) assert num == sum([bincoeff(l, o + 1) for o in range(k)]) + 1, 'wrong number of models for AtMost-{0}-of-{1} ({2})'.format(k, l, e)
def to_cnf(self, x_vars, cnf): y_vars = [cnf.nv + i + 1 for i in range(self.out_dim)] cnf.nv = max(y_vars) for i in range(self.out_dim): C_i = -(self.sigma[i] / self.alpha[i]) * self.gamma[i] + self.mu[i] - self.b[i] if self.alpha[i] > 0: C_i = np.ceil(C_i) elif self.alpha[i] < 0: C_i = np.floor(C_i) print(self.sigma[i], self.alpha[i], self.gamma[i], self.mu[i], self.b[i]) print(C_i) print(np.sum(self.A[i])) C_ = np.ceil(C_i / 2 + np.sum(self.A[i]) / 2) n_nega = sum(self.A[i] > 0) print(C_, n_nega) D = int(C_ + n_nega) print(D) lits = [x if a > 0 else -x for (x, a) in zip(x_vars, self.A[i])] amo = CardEnc.atmost(lits, D - 1, cnf.nv, EncType.seqcounter) r = amo.nv # TODO: Check assumption that this is "r(n, D)" of AAAI paper print("Adding", len(amo.clauses), "AMO clauses") cnf.extend(amo.clauses) cnf.append([-r, y_vars[i]]) cnf.append([r, -y_vars[i]]) # ale = CardEnc.atleast(lits, D, cnf.nv, EncType.seqcounter) # r = ale.nv # TODO: Check assumption that this is "r(n, D)" of AAAI paper # print("Adding", len(ale.clauses), "ALE clauses") print(len(cnf.clauses)) # cnf.extend(ale.clauses) # cnf.append([-r, y_vars[i]]) # cnf.append([ r, -y_vars[i]]) return y_vars
def atmost(self, lits, bound=1, encoding=EncType.seqcounter): """ **Added in SugarRush**\n Uses :meth:`pysat.card.CardEnc.atmost`. Adds automatic bookkeeping of literals. """ cnf = CardEnc.atmost(lits=lits, bound=bound, encoding=encoding, top_id=self._top_id()) clauses = cnf.clauses self._add_lits_from(clauses) return clauses
def naveh_dynamics(self): clauses = [] for t in range(1, self.num_observations): clauses.extend( CardEnc.atmost(self.__get_Q0_predicates(t), bound=self.police, vpool=self.pool).clauses) if self.police == 0: return clauses for t in range(self.num_observations - 1): for num_sick in range(self.cols * self.rows): for sick_tiles in itertools.combinations(self.tiles, num_sick): healthy_tiles = [ tile for tile in self.tiles if tile not in sick_tiles ] for sick_state_perm in itertools.combinations_with_replacement( self.possible_sick_states(t), num_sick): clause = [] for (i, j), state in zip(sick_tiles, sick_state_perm): clause.append(-self.obj2id[f"{state}_{i}_{j}^{t}"]) for i, j in healthy_tiles: for state in self.possible_sick_states(t): clause.append( self.obj2id[f"{state}_{i}_{j}^{t}"]) equals_clauses = CardEnc.equals( lits=self.__get_Q0_predicates(t + 1), bound=min(self.police, num_sick), vpool=self.pool).clauses for sub_clause in equals_clauses: temp_clause = copy.deepcopy(clause) temp_clause += sub_clause clauses.append(temp_clause) return CNF(from_clauses=clauses)
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 test_atmost1(): encs = list( filter(lambda name: not name.startswith('__') and name != 'native', dir(EncType))) for l in range(10, 20): for e in encs: cnf = CardEnc.atmost(lits=list(range(1, l + 1)), bound=1, encoding=getattr(EncType, e)) # enumerating all models with MinisatGH(bootstrap_with=cnf) as solver: for num, model in enumerate(solver.enum_models(), 1): solver.add_clause([-l for l in model[:l]]) assert num == l + 1, 'wrong number of models for AtMost-1-of-{0} ({1})'.format( l, e)
def test_atmost(): vp = IDPool() n = 20 b = 50 assert n <= b lits = [vp.id(v) for v in range(1, n + 1)] top = vp.top G = CardEnc.atmost(lits, b, vpool=vp) assert len(G.clauses) == 0 try: assert vp.top >= top except AssertionError as e: print(f"\nvp.top = {vp.top} (expected >= {top})\n") raise e
def coloring(self, n_color): """ Returns whether or not there exists a vertex coloring of, at most, n_color colors. Accepts one param: - n_color: number of color to check Might raise ValueError exception. """ if n_color < 0: raise ValueError('Number of colors must be positive integer') if n_color == 0: return not bool(self.vertices()) logging.info('\nCodifying SAT Solver...') solver = Solver(name='cd') vpool = IDPool() logging.info( ' -> Codifying: Every vertex must have a color, and only one') for vertex in self.vertices(): cnf = CardEnc.equals(lits=[ vpool.id('{}color{}'.format(vertex, color)) for color in range(n_color) ], vpool=vpool, encoding=0) solver.append_formula(cnf) logging.info( ' -> Codifying: No two neighbours can have the same color') for vertex in self.vertices(): for neighbour in self[vertex]: for color in range(n_color): solver.add_clause([ -vpool.id('{}color{}'.format(vertex, color)), -vpool.id('{}color{}'.format(neighbour, color)) ]) logging.info('Running SAT Solver...') return solver.solve()
def unique_tile_dynamics(self): clauses = [] for i in range(self.rows): for j in range(self.cols): for t in range(self.t_max + 1): # including all legal_states = [ self.pool.obj2id[f"U_{i}_{j}^{t}"], self.pool.obj2id[f"I0_{i}_{j}^{t}"], self.pool.obj2id[f"I_{i}_{j}^{t}"], self.pool.obj2id[f"S0_{i}_{j}^{t}"], self.pool.obj2id[f"S1_{i}_{j}^{t}"], self.pool.obj2id[f"S2_{i}_{j}^{t}"], self.pool.obj2id[f"Q0_{i}_{j}^{t}"], self.pool.obj2id[f"Q1_{i}_{j}^{t}"], self.pool.obj2id[f"H_{i}_{j}^{t}"], ] clauses.extend( CardEnc.equals(legal_states, 1, vpool=self.pool)) return CNF(from_clauses=clauses)
def __call__(self, k, vars_): assert max(map(abs, vars_)) < self.s1, 'max vars exceeded nvars_total' assert len(vars_) == len(set(vars_)), 'vars_ element must be unique' assert all(x > 0 for x in vars_), 'vars_ must be greater than 0' n = len(vars_) cnf = CardEnc.equals(range(1, n + 1), k) C = cnf.clauses # ns: number of auxiliary varibales in C ns = len(set(map(abs, itertools.chain.from_iterable(C)))) - n assert ns >= 0, (ns, n, C, k, vars_) sdiff = self.s1 - (n + 1) assert sdiff >= 0, (self.s1, n, sdiff, k, vars_) if sdiff: C = [[(x + sdiff) if x > n else x for x in r] for r in C] C = [[(x - sdiff) if x < -n else x for x in r] for r in C] tr = dict(zip(range(1, n + 1), vars_)) tr.update(dict(zip(range(-1, -n - 2, -1), [-x for x in vars_]))) C = [[tr.get(x, x) for x in r] for r in C] self.s1 += ns self.logger.debug('Updated self.s1 from %d to %d', self.s1 - ns, self.s1) C = [[int(x) for x in r] for r in C] return C
def make_constraint(self, row_index, col_index): """ Compute equations for field (row_index, col_index) """ adjacent_fields = self.game.get_adjacent_fields(row_index, col_index) adjacent_mines = self.game.get_adjacent_mines(row_index, col_index) assert adjacent_mines for field in adjacent_fields: if field not in self.current_adjacent_fields \ and self.game.board[field.row][field.column].covered \ and field not in self.mines: self.current_adjacent_fields.append(field) literals = [] for field in adjacent_fields: if field.covered: literals.append(field.id) return CardEnc.equals(lits=literals, bound=adjacent_mines, encoding=EncType.native)
def find_hamiltonian_path(self, check_cycle=False): """ should it exists, find a Hamiltonian on current self. Otherwise return empty list. """ if not self.edges(): return [] logging.info('Codifying SAT Solver...') length = len(self.vertices()) solver = Solver(name='cd') names = {} vpool = IDPool() for integer, vertex in enumerate(self.vertices()): names[integer + 1] = vertex names[0] = names[length] for position_in_path in range(length): for vertex in range(1, length + 1): vpool.id(xvar(vertex, position_in_path)) logging.info(' -> Codifying: All Positions occupied') for position_in_path in range(length): var_list = [ vpool.id(xvar(vertex, position_in_path)) for vertex in range(1, length + 1) ] cnf = CardEnc.equals(lits=var_list, vpool=vpool) solver.append_formula(cnf) logging.info(' -> Codifying: All vertex visited') for vertex in range(1, length + 1): var_list = [ vpool.id(xvar(vertex, position_in_path)) for position_in_path in range(length) ] cnf = CardEnc.equals(lits=var_list, vpool=vpool) solver.append_formula(cnf) logging.info(' -> Codifying: Adjacency Matrix') edges = self.edges() for vertex_a in range(1, length + 1): for vertex_b in range(vertex_a + 1, length + 1): if (names[vertex_a], names[vertex_b]) not in edges: for position_in_path in range(length - 1): solver.add_clause([ -vpool.id(xvar(vertex_a, position_in_path)), -vpool.id(xvar(vertex_b, position_in_path + 1)), ]) solver.add_clause([ -vpool.id(xvar(vertex_b, position_in_path)), -vpool.id(xvar(vertex_a, position_in_path + 1)), ]) if check_cycle: solver.add_clause([ vpool.id(xvar(vertex_b, length - 1)), vpool.id(xvar(vertex_a, 0)) ]) solver.add_clause([ vpool.id(xvar(vertex_b, 0)), vpool.id(xvar(vertex_a, length - 1)) ]) logging.info('Running SAT Solver...') solution = [] if solver.solve(): for index, variable in enumerate(solver.get_model()): if variable > 0 and index < length**2: solution.append(names[variable % length]) return solution
def make_formula(n_police, n_medics, n_rows, n_cols, n_time): states = {'U', 'H', 'S', 'I', 'Q'} variables = {} formula = CNF() var_pool = IDPool() for t in range(n_time): for r in range(n_rows): for c in range(n_cols): for s in states: variables[(r, c), t, s] = var_pool.id(f'({r}, {c}), {t}, {s}') variables[(r, c), t, 'P'] = var_pool.id( f'({r}, {c}), {t}, P') # Police were used variables[(r, c), t, 'M'] = var_pool.id( f'({r}, {c}), {t}, M') # Medics were used variables[(r, c), t, 'SS'] = var_pool.id( f'({r}, {c}), {t}, SS') # Stayed sick from last time for t in range(n_time): formula.extend( CardEnc.atmost([ variables[(r, c), t, 'P'] for r in range(n_rows) for c in range(n_cols) ], bound=n_police, vpool=var_pool)) formula.extend( CardEnc.atmost([ variables[(r, c), t, 'M'] for r in range(n_rows) for c in range(n_cols) ], bound=n_medics, vpool=var_pool)) for r in range(n_rows): for c in range(n_cols): formula.extend( CardEnc.equals([variables[(r, c), t, s] for s in states], vpool=var_pool)) if t > 0: formula.extend( req_equiv([ -variables[(r, c), t - 1, 'Q'], variables[(r, c), t, 'Q'] ], [variables[(r, c), t, 'P']])) formula.extend( req_equiv([ -variables[(r, c), t - 1, 'I'], variables[(r, c), t, 'I'] ], [variables[(r, c), t, 'M']])) formula.extend( req_equiv([ variables[(r, c), t - 1, 'S'], variables[(r, c), t, 'S'] ], [variables[(r, c), t, 'SS']])) nearby_sick_condition = [] for r_, c_ in nearby(r, c, n_rows, n_cols): nearby_sick_condition.append(variables[(r_, c_), t, 'SS']) formula.extend( req_imply([ variables[(r, c), t, 'SS'], variables[(r_, c_), t - 1, 'H'] ], [ variables[(r_, c_), t, 'S'], variables[(r_, c_), t, 'I'] ])) # formula.extend(req_imply([variables[(r, c), t, 'SS']], [-variables[(r_, c_), t, 'H']])) formula.extend( req_imply([ variables[(r, c), t - 1, 'H'], variables[(r, c), t, 'S'] ], nearby_sick_condition)) if t + 1 < n_time: formula.extend( req_equiv([variables[(r, c), t, 'U']], [variables[(r, c), t + 1, 'U']])) formula.extend( req_imply([variables[(r, c), t, 'I']], [variables[(r, c), t + 1, 'I']])) formula.extend( req_imply([variables[(r, c), t + 1, 'S']], [ variables[(r, c), t, 'S'], variables[(r, c), t, 'H'] ])) formula.extend( req_imply([variables[(r, c), t + 1, 'Q']], [ variables[(r, c), t, 'Q'], variables[(r, c), t, 'S'] ])) if t == 0: formula.append([-variables[(r, c), t, 'Q']]) formula.append([-variables[(r, c), t, 'I']]) if t + 1 < n_time: formula.extend( req_imply([variables[(r, c), t, 'S']], [ variables[(r, c), t + 1, 'S'], variables[(r, c), t + 1, 'Q'] ])) formula.extend( req_imply([variables[(r, c), t, 'Q']], [variables[(r, c), t + 1, 'Q']])) if t + 2 < n_time: formula.extend( req_imply([ variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'S'] ], [ variables[(r, c), t + 2, 'S'], variables[(r, c), t + 2, 'Q'] ])) formula.extend( req_imply([ variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'Q'] ], [variables[(r, c), t + 2, 'Q']])) formula.extend( req_imply([variables[(r, c), t, 'Q']], [variables[(r, c), t + 2, 'H']])) if t + 3 < n_time: formula.extend( req_imply([ variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'S'], variables[(r, c), t + 2, 'S'] ], [variables[(r, c), t + 3, 'H']])) if 0 < t and t + 1 < n_time: formula.extend( req_imply([ -variables[(r, c), t - 1, 'S'], variables[(r, c), t, 'S'] ], [ variables[(r, c), t + 1, 'S'], variables[(r, c), t + 1, 'Q'] ])) formula.extend( req_imply([ -variables[(r, c), t - 1, 'Q'], variables[(r, c), t, 'Q'] ], [variables[(r, c), t + 1, 'Q']])) if 0 < t and t + 2 < n_time: formula.extend( req_imply([ -variables[(r, c), t - 1, 'S'], variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'S'] ], [ variables[(r, c), t + 2, 'S'], variables[(r, c), t + 2, 'Q'] ])) formula.extend( req_imply([ -variables[(r, c), t - 1, 'S'], variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'Q'] ], [variables[(r, c), t + 2, 'Q']])) formula.extend( req_imply([ -variables[(r, c), t - 1, 'Q'], variables[(r, c), t, 'Q'] ], [variables[(r, c), t + 2, 'H']])) if 0 < t and t + 3 < n_time: formula.extend( req_imply([ -variables[(r, c), t - 1, 'S'], variables[(r, c), t, 'S'], variables[(r, c), t + 1, 'S'], variables[(r, c), t + 2, 'S'] ], [variables[(r, c), t + 3, 'H']])) return var_pool, formula
def buildClausesAndEffects(b, nRows, nCols, nPolice, nMedics, actionToIndex, atomsToIndex): actionClauses = [] mapIndicesList = buildMapIndices(nRows, nCols) DeseaseSpread = AllPossibleInfectionIndices(nRows, nCols) actionEffectsAtPreviousT = {} for roundT in range(b - 1): actionEffectsAtT = {} for idx in mapIndicesList: for p in range(nPolice): actionIndex = actionToIndex[("Q", p, roundT, idx)] curClauses, curPre, curAdd, curDel = modelAgentQAction( b, actionIndex, roundT, idx, atomsToIndex) actionEffectsAtT[actionIndex] = (curPre, curAdd, curDel) actionClauses += curClauses for m in range(nMedics): actionIndex = actionToIndex[("V", m, roundT, idx)] curClauses, curPre, curAdd, curDel = modelAgentVAction( b, actionIndex, roundT, idx, atomsToIndex) actionEffectsAtT[actionIndex] = (curPre, curAdd, curDel) actionClauses += curClauses for idx in mapIndicesList: # at most 1 quarantine and at most 1 vaccination in each place in the map qvars = [ actionToIndex[("Q", p, roundT, idx)] for p in range(nPolice) ] ivars = [ actionToIndex[("V", m, roundT, idx)] for m in range(nMedics) ] actionClauses += CardEnc.atmost(lits=qvars, encoding=EncType.pairwise, bound=1).clauses actionClauses += CardEnc.atmost(lits=ivars, encoding=EncType.pairwise, bound=1).clauses for p in range( nPolice): # each police can be used at most once each turn qvars = [ actionToIndex[("Q", p, roundT, idx)] for idx in mapIndicesList ] actionClauses += CardEnc.atmost(lits=qvars, encoding=EncType.pairwise, bound=1).clauses for m in range( nMedics): # each medic can be used at most once each turn ivars = [ actionToIndex[("V", m, roundT, idx)] for idx in mapIndicesList ] actionClauses += CardEnc.atmost(lits=ivars, encoding=EncType.pairwise, bound=1).clauses for p in range(nPolice): actionClauses += [[ -actionToIndex[("Q", p, roundT, idx)], atomsToIndex[("Q", p, roundT)] ] for idx in mapIndicesList] actionClauses += [[-atomsToIndex[("Q", p, roundT)]] + [ actionToIndex[("Q", p, roundT, idx)] for idx in mapIndicesList ]] for m in range(nMedics): actionClauses += [[ -actionToIndex[("V", m, roundT, idx)], atomsToIndex[("V", m, roundT)] ] for idx in mapIndicesList] actionClauses += [[-atomsToIndex[("V", m, roundT)]] + [ actionToIndex[("V", m, roundT, idx)] for idx in mapIndicesList ]] for pair in DeseaseSpread: actionIndex = actionToIndex[(roundT, pair)] curClauses, curPre, curAdd, curDel = ModelInfection( b, roundT, pair, atomsToIndex, actionIndex) actionEffectsAtT[actionIndex] = (curPre, curAdd, curDel) actionClauses += curClauses for atom, atomIdx in atomsToIndex.items(): if atom[0] == roundT: add = atomsToIndex[(atom[0] + 1, atom[1], atom[2])] cur_state = atom[2] actionIdx = actionToIndex[atomIdx] actionEffectsAtT[actionToIndex[atomIdx]] = ([atomIdx], [add], []) actionClauses.append([-actionIdx, atomIdx]) if cur_state == "H": required = [ atomsToIndex[("V", m, roundT)] for m in range(nMedics) ] for req in required: actionClauses.append([req, -actionIdx]) row, col = atom[1][0], atom[1][1] for neighbor in [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]: if 0 <= neighbor[0] <= nRows - 1 and 0 <= neighbor[ 1] <= nCols - 1: actionClauses.append([ -actionIdx, atomsToIndex[(roundT + 1, neighbor, "Q")], -atomsToIndex[(roundT, neighbor, "S")] ]) if cur_state == "Q": if roundT >= 1: preC = [ atomsToIndex[(roundT, atom[1], "Q")], atomsToIndex[(roundT - 1, atom[1], "Q")] ] clause = [-actionIdx] for pre in preC: clause.append(-pre) actionClauses.append(clause) if cur_state == "S": required = [ atomsToIndex[("Q", p, roundT)] for p in range(nPolice) ] for req in required: actionClauses.append([req, -actionIdx]) if roundT >= 2: preC = [ atomsToIndex[(roundT, atom[1], "S")], atomsToIndex[(roundT - 1, atom[1], "S")], atomsToIndex[(roundT - 2, atom[1], "S")] ] clause = [-actionIdx] for pre in preC: clause.append(-pre) actionClauses.append(clause) if roundT >= 2: for idx in mapIndicesList: actionIndex = actionToIndex[("heal", roundT, idx)] curClauses, curPre, curAdd, curDel = ModelHealing( roundT, idx, atomsToIndex, actionIndex) actionEffectsAtT[actionIndex] = (curPre, curAdd, curDel) actionClauses += curClauses if roundT >= 1: for idx in mapIndicesList: actionIndex = actionToIndex[("exitQ", roundT, idx)] curClauses, curPre, curAdd, curDel = ModelExitQ( roundT, idx, atomsToIndex, actionIndex) actionEffectsAtT[actionIndex] = (curPre, curAdd, curDel) actionClauses += curClauses interferClauses = BuildInterferClauses(actionEffectsAtT) actionClauses += interferClauses if roundT >= 1: factAchieveClauses = BuildFactAchieveClauses( actionEffectsAtPreviousT, atomsToIndex, roundT) actionClauses += factAchieveClauses actionEffectsAtPreviousT = actionEffectsAtT if actionEffectsAtPreviousT != {}: factAchieveClauses = BuildFactAchieveClauses(actionEffectsAtPreviousT, atomsToIndex, b - 1) actionClauses += factAchieveClauses return actionClauses
def closest_string(bitarray_list, distance=4): """ Return if a bitarray exists of distance at most 'distance'. Use example: s1=bitarray('0010') s2=bitarray('0011') closest_string([s1,s2], distance=0) > False closest_string([s1,s2], distance=2) > True """ if distance < 0: raise ValueError('Distance must be positive integer') logging.info('\nCodifying SAT Solver...') length = max(len(bit_arr) for bit_arr in bitarray_list) solver = Solver(name='mcm') vpool = IDPool() local_list = bitarray_list.copy() logging.info(' -> Codifying: normalizing strings') for index, bitarr in enumerate(bitarray_list): aux = (length - len(bitarr)) * bitarray('0') local_list[index] = bitarr + aux logging.info(' -> Codifying: imposing distance condition') for index, word in enumerate(local_list): for pos in range(length): vpool.id(ut.xvar(index, pos)) for pos in range(length): vpool.id(ut.yvar(pos)) for index, word in enumerate(local_list): for pos in range(length): vpool.id(ut.zvar(index, pos)) for index, word in enumerate(local_list): for pos in range(length): for clause in ut.triple_equal(ut.xvar(index, pos), ut.yvar(pos), ut.zvar(index, pos), vpool=vpool): solver.add_clause(clause) cnf = CardEnc.atleast( lits=[vpool.id(ut.zvar(index, pos)) for pos in range(length)], bound=length - distance, vpool=vpool) solver.append_formula(cnf) logging.info(' -> Codifying: Words Value') assumptions = [] for index, word in enumerate(local_list): for pos in range(length): assumptions += [ vpool.id(ut.xvar(index, pos)) * (-1)**(not word[pos]) ] logging.info('Running SAT Solver...') return solver.solve(assumptions=assumptions)
def generate_police_clauses(self): clauses = [] for turn in range(1, self.num_turns): lits = [ self.vpool.id((turn, row, col, QUARANTINED_1)) for row in range(self.height) for col in range(self.width) ] clauses.extend( CardEnc.atmost(lits, bound=self.num_police, vpool=self.vpool).clauses) # TODO check case of 0 policemen if self.num_police == 0: return clauses for turn in range(self.num_turns - 1): for num_sick in range(self.width * self.height): for sick_tiles in itertools.combinations(self.tiles, num_sick): healthy_tiles = [ tile for tile in self.tiles if tile not in sick_tiles ] # TODO don't iterate over all sick states for sick_state_perm in \ itertools.combinations_with_replacement(self.possible_sick_states(turn), num_sick): clause = [] for (row, col), state in zip(sick_tiles, sick_state_perm): clause.append(-self.vpool.id((turn, row, col, state))) for row, col in healthy_tiles: for state in self.possible_sick_states(turn): clause.append( self.vpool.id((turn, row, col, state))) lits = [ self.vpool.id((turn + 1, row, col, QUARANTINED_1)) for row, col in sick_tiles ] equals_clauses = CardEnc.equals( lits, bound=min(self.num_police, num_sick), vpool=self.vpool).clauses for sub_clause in equals_clauses: temp_clause = deepcopy(clause) temp_clause += sub_clause clauses.append(temp_clause) # if num_sick <= self.num_police: # for (row, col) in sick_tiles: # temp_clause = deepcopy(clause) # temp_clause.append(self.vpool.id((turn+1, row, col, QUARANTINED_1))) # clauses.extend(temp_clause) # # # for (row, col) in healthy_tiles: # # temp_clause = deepcopy(clause) # # temp_clause.append(-self.vpool.id((turn+1, row, col, QUARANTINED_1))) # # clauses.extend(temp_clause) # # else: # lits = [self.vpool.id((turn+1, row, col, QUARANTINED_1)) # for row in range(self.height) # for col in range(self.width)] # equals_clauses = CardEnc.equals(lits, bound=self.num_police, vpool=self.vpool) # # for sub_clause in equals_clauses.clauses(): # temp_clause = deepcopy(clause) # temp_clause += sub_clause # clauses.extend(temp_clause) return clauses
def solve(self): n_orange = len([h for h in self.hexagons if h.tpe == HexagonTypes.ORANGE]) use_total = False while True: with Solver() as s: if use_total: assert self.total_n_blue is not None, 'Need total number of blue to solve' is_orange = [h.var_id for h in self.hexagons if h.tpe == HexagonTypes.ORANGE] s.append_formula(CardEnc.equals(is_orange, self.total_n_blue - self.n_blue, vpool=id_pool).clauses) print('Total active') assumptions = [] formulas = [] for h in self.hexagons: formulas.append((h.var_id, h.constraint)) if h.constraint is not None: s.append_formula(h.constraint) if h.tpe == HexagonTypes.DARK: assumptions.append(-h.var_id) elif h.tpe in [HexagonTypes.BLUE, HexagonTypes.BLUE_W_NUMBER]: assumptions.append(h.var_id) # s.solve(assumptions=assumptions) for l in self.lines: formulas.append((l.detection, h.constraint)) if l.constraint is not None: s.append_formula(l.constraint) # assert s.solve(assumptions=assumptions) if not s.solve(assumptions=assumptions): raise RuntimeError('The boolean formula is unsatisfiable. This usually occurs when a game element is detected wrongly.') hexagons_to_update = [] for h in reversed(self.hexagons): if h.tpe != HexagonTypes.ORANGE: continue if not s.solve(assumptions=assumptions + [h.var_id]): print(-h.var_id) pyautogui.rightClick(*tuple(h.center + self.window_rect[:2])) hexagons_to_update.append(h) elif not s.solve(assumptions=assumptions + [-h.var_id]): print(h.var_id) pyautogui.leftClick(*tuple(h.center + self.window_rect[:2])) hexagons_to_update.append(h) self.n_blue += 1 n_orange -= len(hexagons_to_update) if n_orange == 0: break if self.total_n_blue is not None: print('REMAINING', self.total_n_blue - self.n_blue) if len(hexagons_to_update) == 0: if use_total: raise RuntimeError('Can\'t solve the puzzle! Solution is ambiguous. Maybe a game element was detected wrongly.') use_total = True continue sleep(.3) start_time = time() while len(hexagons_to_update) > 0: img = self.take_screenshot() failed = [] for h in hexagons_to_update: print('updating', h.var_id) failed.append(not h.update(img)) hexagons_to_update = list(compress(hexagons_to_update, failed)) if time() - start_time >= 3: raise RuntimeError('Failed to update state of game elements after 3 seconds of trying') for x in self.hexagons + self.lines: x.check_if_constraint_trivial()
def encode_constraint( number, # (int) number of hexagons in influence which should be blue curly, # (bool) is curly dashed, # (bool) is dashed influence, # (List[Hexagon]) list of hexagons which are affected by the constraint circle # (bool) if influenced hexagons are in a circle ): assert not circle or len(influence) <= 6 assert len(influence) >= number # extract boolean variables literals = [h.var_id for h in influence] # if just a number if not curly and not dashed: return CardEnc.equals(lits=literals, bound=number, vpool=id_pool) # if dashed or curly # encode {number} in dnf n_vars = len(influence) dnf = [] # this case needs special considerations since for hexagons gaps to count as such if (dashed or curly) and circle and n_vars < 6: # first find partitions # find start # remember: influence is sorted start = None for i in range(n_vars): # if there is a gap between influence[i] and influence[i - 1] if influence[i - 1] not in influence[i].influence or influence[i - 1].distance(influence[i]) > 1: start = i break assert start is not None # rotate such that start is at the beginning of influence influence = influence[start:] + influence[:start] partitions = [[influence[0]]] for i in influence[1:]: # if there is no gap between i and partitions[-1][-1] if i in partitions[-1][-1].influence and partitions[-1][-1].distance(i) == 1: partitions[-1].append(i) else: partitions.append([i]) # encode {number} in dnf all_not = [-h.var_id for h in influence] start = 0 for p in partitions: if len(p) < number: start += len(p) continue for i in range((len(p) - number + 1)): formula = all_not.copy() for j in range(number): formula[start + i + j] = -formula[start + i + j] dnf.append(formula) start += len(p) else: # encode {number} in dnf all_not = [-h.var_id for h in influence] for i in range(n_vars if circle else (n_vars - number + 1)): formula = all_not.copy() for j in range(number): formula[(i + j) % n_vars] = -formula[(i + j) % n_vars] dnf.append(formula) if dashed: # -number- == not {number} and number cnf = CardEnc.equals(lits=literals, bound=number, vpool=id_pool) cnf.extend([[-v for v in c] for c in dnf]) # not {number} with DeMorgan return cnf if curly: # dnf to cnf cnf = [[]] for c in dnf: aux_var = id_pool.id(id_pool.top + 1) cnf[0].append(aux_var) for v in c: cnf.append([-aux_var, v]) return CNF(from_clauses=cnf)
from pysat.formula import IDPool from pysat.card import CardEnc vp = IDPool() n = 20 b = 50 assert n <= b lits = [vp.id(v) for v in range(1, n + 1)] top = vp.top G = CardEnc.atmost(lits, b, vpool=vp) assert len(G.clauses) == 0 try: assert vp.top >= top except AssertionError as e: print(f"\nvp.top = {vp.top} (expected >= {top})\n") raise e