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 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 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 equals(self, lits, bound=1, encoding=EncType.seqcounter): """ **Added in SugarRush**\n Uses :meth:`pysat.card.CardEnc.equals`. Adds automatic bookkeeping of literals. """ cnf = CardEnc.equals(lits=lits, bound=bound, encoding=encoding, top_id=self._top_id()) clauses = cnf.clauses self._add_lits_from(clauses) return clauses
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 test_equals1(): encs = list( filter(lambda name: not name.startswith('__') and name != 'native', dir(EncType))) for l in range(10, 20): for e in encs: for b in (1, l - 1): cnf = CardEnc.equals(lits=list(range(1, l + 1)), bound=b, 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, 'wrong number of models for Equals-1-of-{0} ({1})'.format( l, 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 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 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 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 __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 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 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 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 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)
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