Esempio n. 1
0
    def atmost(cls, lits, bound=1, top_id=None, vpool=None,
            encoding=EncType.seqcounter):
        """
            This method can be used for creating a CNF encoding of an AtMostK
            constraint, i.e. of :math:`\sum_{i=1}^{n}{x_i}\leq k`. The method
            shares the arguments and the return type with method
            :meth:`CardEnc.atleast`. Please, see it for details.
        """

        if encoding < 0 or encoding > 9:
            raise(NoSuchEncodingError(encoding))

        # checking if the bound is meaningless for any encoding
        if bound < 0:
            raise ValueError('Wrong bound: {0}'.format(bound))

        if encoding in (0, 4, 5) and 1 < bound < len(lits) - 1:
            raise(UnsupportedBound(encoding, bound))

        assert not top_id or not vpool, \
                'Use either a top id or a pool of variables but not both.'

        # we are going to return this formula
        ret = CNFPlus()

        # if the list of literals is empty, return empty formula
        if not lits:
            return ret

        # obtaining the top id from the variable pool
        if vpool:
            top_id = vpool.top

        # making sure we are dealing with a list of literals
        lits = list(lits)

        # choosing the maximum id among the current top and the list of literals
        top_id = max(map(lambda x: abs(x), lits + [top_id if top_id != None else 0]))

        # MiniCard's native representation is handled separately
        if encoding == 9:
            ret.atmosts, ret.nv = [(lits, bound)], top_id
            return ret

        res = pycard.encode_atmost(lits, bound, top_id, encoding,
                int(MainThread.check()))

        if res:
            ret.clauses, ret.nv = res

            # updating vpool if necessary
            if vpool:
                if vpool._occupied and vpool.top <= vpool._occupied[0][0] <= ret.nv:
                    cls._update_vids(ret, vpool)
                else:
                    # here, ret.nv id is assumed to be larger than the top id
                    vpool.top = ret.nv - 1
                    vpool._next()

        return ret
Esempio n. 2
0
    def atmost(cls, lits, bound=1, top_id=None, encoding=EncType.seqcounter):
        """
            This method can be used for creating a CNF encoding of an AtMostK
            constraint, i.e. of :math:`\sum_{i=1}^{n}{x_i}\leq k`. The method
            shares the arguments and the return type with method
            :meth:`CardEnc.atleast`. Please, see it for details.
        """

        if encoding < 0 or encoding > 9:
            raise (NoSuchEncodingError(encoding))

        if not top_id:
            top_id = max(map(lambda x: abs(x), lits))

        # we are going to return this formula
        ret = CNFPlus()

        # MiniCard's native representation is handled separately
        if encoding == 9:
            ret.atmosts, ret.nv = [(lits, bound)], top_id
            return ret

        # saving default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_DFL)

        res = pycard.encode_atmost(lits, bound, top_id, encoding)

        # recovering default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, def_sigint_handler)

        if res:
            ret.clauses, ret.nv = res

        return ret
Esempio n. 3
0
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
Esempio n. 4
0
    def atmost(cls,
               lits,
               bound=1,
               top_id=None,
               vpool=None,
               encoding=EncType.seqcounter):
        """
            This method can be used for creating a CNF encoding of an AtMostK
            constraint, i.e. of :math:`\sum_{i=1}^{n}{x_i}\leq k`. The method
            shares the arguments and the return type with method
            :meth:`CardEnc.atleast`. Please, see it for details.
        """

        if encoding < 0 or encoding > 9:
            raise (NoSuchEncodingError(encoding))

        assert not top_id or not vpool, \
                'Use either a top id or a pool of variables but not both.'

        # we are going to return this formula
        ret = CNFPlus()

        # if the list of literals is empty, return empty formula
        if not lits:
            return ret

        # obtaining the top id from the variable pool
        if vpool:
            top_id = vpool.top

        if not top_id:
            top_id = max(map(lambda x: abs(x), lits))

        # MiniCard's native representation is handled separately
        if encoding == 9:
            ret.atmosts, ret.nv = [(lits, bound)], top_id
            return ret

        # saving default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_DFL)

        res = pycard.encode_atmost(lits, bound, top_id, encoding)

        # recovering default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, def_sigint_handler)

        if res:
            ret.clauses, ret.nv = res

        # updating vpool if necessary
        if vpool:
            if vpool._occupied and vpool.top <= vpool._occupied[0][0] <= ret.nv:
                cls._update_vids(ret, vpool)
            else:
                vpool.top = ret.nv - 1
                vpool._next()

        return ret
Esempio n. 5
0
 def __init__(self, k):
     self.map_atoms = {}
     self.atoms_counter = 1
     self.cnf = CNFPlus()
     self.min_card = math.inf
     self.number_of_diagnoses = 0
     self.time = 0
     self.k = k
     self.atmost_cluse = []
Esempio n. 6
0
    print('        -s, --solver     SAT solver to use')
    print('                         Available values: g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default = m22)')
    print('        -v, --verbose    Be verbose')


#
#==============================================================================
if __name__ == '__main__':
    solver, cardenc, verbose, files = parse_options()

    if files:
        # parsing the input formula
        if re.search('\.wcnf[p|+]?(\.(gz|bz2|lzma|xz))?$', files[0]):
            formula = WCNFPlus(from_file=files[0])
        else:  # expecting '*.cnf[,p,+].*'
            formula = CNFPlus(from_file=files[0]).weighted()

        with FM(formula, solver=solver, enc=cardenc, verbose=verbose) as fm:
            res = fm.compute()

            if res:
                print('s OPTIMUM FOUND')
                print('o {0}'.format(fm.cost))

                if verbose > 2:
                    print('v', ' '.join([str(l) for l in fm.model]), '0')
            else:
                print('s UNSATISFIABLE')

            if verbose > 1:
                print('c oracle time: {0:.4f}'.format(fm.oracle_time()))
Esempio n. 7
0
    def atleast(cls,
                lits,
                bound=1,
                top_id=None,
                vpool=None,
                encoding=EncType.seqcounter):
        """
            This method can be used for creating a CNF encoding of an AtLeastK
            constraint, i.e. of :math:`\sum_{i=1}^{n}{x_i}\geq k`. The method
            takes 1 mandatory argument ``lits`` and 3 default arguments can be
            specified: ``bound``, ``top_id``, ``vpool``, and ``encoding``.

            :param lits: a list of literals in the sum.
            :param bound: the value of bound :math:`k`.
            :param top_id: top variable identifier used so far.
            :param vpool: variable pool for counting the number of variables.
            :param encoding: identifier of the encoding to use.

            :type lits: iterable(int)
            :type bound: int
            :type top_id: integer or None
            :type vpool: :class:`.IDPool`
            :type encoding: integer

            Parameter ``top_id`` serves to increase integer identifiers of
            auxiliary variables introduced during the encoding process. This
            is helpful when augmenting an existing CNF formula with the new
            cardinality encoding to make sure there is no collision between
            identifiers of the variables. If specified, the identifiers of the
            first auxiliary variable will be ``top_id+1``.

            Instead of ``top_id``, one may want to use a pool of variable
            identifiers ``vpool``, which is automatically updated during the
            method call. In many circumstances, this is more convenient than
            using ``top_id``. Also note that parameters ``top_id`` and
            ``vpool`` **cannot** be specified *simultaneusly*.

            The default value of ``encoding`` is :attr:`Enctype.seqcounter`.

            The method *translates* the AtLeast constraint into an AtMost
            constraint by *negating* the literals of ``lits``, creating a new
            bound :math:`n-k` and invoking :meth:`CardEnc.atmost` with the
            modified list of literals and the new bound.

            :raises CardEnc.NoSuchEncodingError: if encoding does not exist.

            :rtype: a :class:`.CNFPlus` object where the new \
            clauses (or the new native atmost constraint) are stored.
        """

        if encoding < 0 or encoding > 9:
            raise (NoSuchEncodingError(encoding))

        assert not top_id or not vpool, \
                'Use either a top id or a pool of variables but not both.'

        # we are going to return this formula
        ret = CNFPlus()

        # if the list of literals is empty, return empty formula
        if not lits:
            return ret

        # obtaining the top id from the variable pool
        if vpool:
            top_id = vpool.top

        if not top_id:
            top_id = max(map(lambda x: abs(x), lits))

        # Minicard's native representation is handled separately
        if encoding == 9:
            ret.atmosts, ret.nv = [([-l for l in lits], len(lits) - bound)
                                   ], top_id
            return ret

        # saving default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_DFL)

        res = pycard.encode_atleast(lits, bound, top_id, encoding)

        # recovering default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, def_sigint_handler)

        if res:
            ret.clauses, ret.nv = res

        # updating vpool if necessary
        if vpool:
            if vpool._occupied and vpool.top <= vpool._occupied[0][0] <= ret.nv:
                cls._update_vids(ret, vpool)
            else:
                vpool.top = ret.nv - 1
                vpool._next()

        return ret
Esempio n. 8
0
    def atleast(cls, lits, bound=1, top_id=None, encoding=EncType.seqcounter):
        """
            This method can be used for creating a CNF encoding of an AtLeastK
            constraint, i.e. of :math:`\sum_{i=1}^{n}{x_i}\geq k`. The method
            takes 1 mandatory argument ``lits`` and 3 default arguments can be
            specified: ``bound``, ``top_id``, and ``encoding``.

            :param lits: a list of literals in the sum.
            :param bound: the value of bound :math:`k`.
            :param top_id: top variable identifier used so far.
            :param encoding: identifier of the encoding to use.

            :type lits: iterable(int)
            :type bound: int
            :type top_id: integer or None
            :type encoding: integer

            Parameter ``top_id`` serves to increase integer identifiers of
            auxiliary variables introduced during the encoding process. This is
            helpful when augmenting an existing CNF formula with the new
            cardinality encoding to make sure there is no collision between
            identifiers of the variables. If specified the identifiers of the
            first auxiliary variable will be ``top_id+1``.

            The default value of ``encoding`` is :attr:`Enctype.seqcounter`.

            The method *translates* the AtLeast constraint into an AtMost
            constraint by *negating* the literals of ``lits``, creating a new
            bound :math:`n-k` and invoking :meth:`CardEnc.atmost` with the
            modified list of literals and the new bound.

            :raises CardEnc.NoSuchEncodingError: if encoding does not exist.

            :rtype: a :class:`.CNFPlus` object where the new \
            clauses (or the new native atmost constraint) are stored.
        """

        if encoding < 0 or encoding > 9:
            raise (NoSuchEncodingError(encoding))

        # we are going to return this formula
        ret = CNFPlus()

        # if the list of literals is empty, return empty formula
        if not lits:
            return ret

        if not top_id:
            top_id = max(map(lambda x: abs(x), lits))

        # Minicard's native representation is handled separately
        if encoding == 9:
            ret.atmosts, ret.nv = [([-l for l in lits], len(lits) - bound)
                                   ], top_id
            return ret

        # saving default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_DFL)

        res = pycard.encode_atleast(lits, bound, top_id, encoding)

        # recovering default SIGINT handler
        def_sigint_handler = signal.signal(signal.SIGINT, def_sigint_handler)

        if res:
            ret.clauses, ret.nv = res

        return ret
Esempio n. 9
0
class MinimalSubset_2:
    def __init__(self, k):
        self.map_atoms = {}
        self.atoms_counter = 1
        self.cnf = CNFPlus()
        self.min_card = math.inf
        self.number_of_diagnoses = 0
        self.time = 0
        self.k = k
        self.atmost_cluse = []

    def add_soft(self, c):
        if self.map_atoms.get(c) is None:
            self.map_atoms[c] = self.atoms_counter
            self.atoms_counter += 1
        self.cnf.append([self.map_atoms[c], self.k])

    def add_atmost(self, ls):
        self.atmost_cluse = ls

    def create_dictionary(self, statement):
        atoms = statement.atoms()
        for letter in atoms:
            if self.map_atoms.get(str(letter)) is None:
                self.map_atoms[str(letter)] = self.atoms_counter
                self.atoms_counter = self.atoms_counter + 1

    def convert_letters_to_integer(self, atom):
        key = atom.replace('~', '')
        int_value = self.map_atoms[key]
        if '~' in atom:
            return int_value * -1
        return int_value

    def convert_integer_to_letters(self, integer):
        for k, v in self.map_atoms.items():
            if v == abs(integer):
                if integer < 0:
                    return '~' + k
                else:
                    return k

    def convert_statement(self, statement):
        statement_cnf = str(statement)
        statement_cnf = statement_cnf.replace('(', "")
        statement_cnf = statement_cnf.replace(')', "")
        list_statements = statement_cnf.split('&')
        literals_list = []
        res = []
        for s in list_statements:
            literals_list.append(s.split('|'))
        for k in literals_list:
            clu = []
            for x_temp in k:
                x_temp = x_temp.replace(" ", "")
                clu.append(self.convert_letters_to_integer(x_temp))
            res.append(clu)
            self.cnf.append(clu)
        return res

    def find_mini_card(self):
        solver = Minicard(bootstrap_with=self.cnf)
        solver.add_atmost(self.atmost_cluse, self.k)
        flag = True
        while flag:
            flag = solver.solve()
            ans = solver.get_model()
            if self.k == 0:
                self.k += 1
                self.min_card = self.k
                break
            if not flag:
                self.k += 1
                self.min_card = self.k + 1
                break
            if flag:
                self.k = self.k - 1
                solver = Minicard(bootstrap_with=self.cnf)
                solver.add_atmost(self.atmost_cluse, self.k)

    def run_solver(self):
        start_time = time.time()
        current_time = None
        self.find_mini_card()
        solver = Minicard(bootstrap_with=self.cnf)
        solver.add_atmost(self.atmost_cluse, self.k)
        while solver.solve():
            ans = solver.get_model()
            ans = [self.convert_integer_to_letters(x) for x in ans]
            counter = 0
            for x in ans:
                if x.startswith('~') and 'gate' in x:
                    counter = counter + 1
                    solver.add_clause(
                        [abs(self.convert_letters_to_integer(x))])
            if counter == 0:
                break
            self.number_of_diagnoses = self.number_of_diagnoses + 1
            current_time = time.time()
            if (current_time - start_time) >= 60:
                print('finish run out of time')
                self.time = np.nan
                self.min_card = np.nan
                self.number_of_diagnoses = np.nan
                return
        if current_time is None:
            current_time = time.time()
        self.time = current_time - start_time
Esempio n. 10
0
    """

    print('Usage:', os.path.basename(sys.argv[0]), '[options] dimacs-file')
    print('Options:')
    print(
        '        -e, --enum=<int>         Compute at most this number of models'
    )
    print(
        '                                 Available values: [1 .. INT_MAX], all (default: 1)'
    )
    print('        -h, --help               Show this message')
    print('        -s, --solver=<string>    SAT solver to use')
    print(
        '                                 Available values: cd, g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default = g3)'
    )


#
#==============================================================================
if __name__ == '__main__':
    # parsing command-line options
    to_enum, solver, files = parse_options()

    # reading an input formula either from a file or from stdin
    if files:
        formula = CNFPlus(from_file=files[0])
    else:
        formula = CNFPlus(from_fp=sys.stdin)

    enumerate_models(formula, to_enum, solver)
Esempio n. 11
0
def _build_clauses(initial, var, vpool):
    num_medics = initial['medics']
    num_police = initial['police']
    observations = initial['observations']
    num_turns = len(observations)
    map = initial['observations'][0]
    num_rows = len(map)
    num_cols = len(map[0])
    clauses = []
    cell_types = ['S', 'H', 'U']
    cell_types = cell_types + ['I'] if num_medics > 0 else cell_types
    cell_types = cell_types + ['Q'] if num_police > 0 else cell_types

    cnf = CNFPlus()

    observations = update_observations(observations, num_turns)

    for turn, obs in enumerate(observations):
        for i, row in enumerate(obs):
            for j, cell_type in enumerate(row):

                # always_relevant_clauses
                clauses.extend(_always_relevant_clauses(vpool, var, cell_types, cell_type, i, j, turn, num_turns, num_rows, num_cols))

                # ###################### End of always true clauses #######################

                # Make sure no I and Q on turn 0
                if turn == 0:
                    if num_medics > 0:
                        clauses.append([-var(t='I', pos=(i, j), turn=0)])
                    if num_police > 0:
                        clauses.append([-var(t='Q', pos=(i, j), turn=0)])

                # Handle S timer, clauses related to police
                if turn + 1 < num_turns:
                    s_timer_clauses = []
                    if num_police == 0:
                        # If S2 at turn x --> S1 at turn x+1
                        s_timer_clauses.append([-var(t='S2', pos=(i, j), turn=turn), var('S1', pos=(i, j), turn=turn + 1)])
                        # If S1 at turn x --> S0 at turn x+1
                        s_timer_clauses.append([-var(t='S1', pos=(i, j), turn=turn), var('S0', pos=(i, j), turn=turn + 1)])
                        # If S0 at turn x --> H at turn x+1
                        s_timer_clauses.append([-var(t='S0', pos=(i, j), turn=turn), var('H', pos=(i, j), turn=turn + 1)])

                    else:
                        # If S2 at turn x --> S1 or Q1 at turn x+1
                        s_timer_clauses.append([-var(t='S2', pos=(i, j), turn=turn),
                                                var('S1', pos=(i, j), turn=turn + 1), var('Q1', pos=(i, j), turn=turn + 1)])
                        # If S1 at turn x --> S0 or Q1 at turn x+1
                        s_timer_clauses.append([-var(t='S1', pos=(i, j), turn=turn),
                                                var('S0', pos=(i, j), turn=turn + 1), var('Q1', pos=(i, j), turn=turn + 1)])
                        # If S0 at turn x --> H or Q1 at turn x+1
                        s_timer_clauses.append([-var(t='S0', pos=(i, j), turn=turn),
                                                var('H', pos=(i, j), turn=turn + 1), var('Q1', pos=(i, j), turn=turn + 1)])
                    clauses.extend(s_timer_clauses)

                # Handle Q timer
                if num_police > 0:
                    if turn == 1:
                        # Q >> Q1
                        q_timer_clauses = [[-var('Q', pos=(i, j), turn=turn),
                                            var('Q1', pos=(i, j), turn=turn)]]
                    elif turn > 1:
                        # Q >> (Q0 | Q1)
                        q_timer_clauses = [[-var('Q', pos=(i, j), turn=turn),
                                            var('Q0', pos=(i, j), turn=turn),
                                            var('Q1', pos=(i, j), turn=turn)]]
                    if turn >= 1:
                        # (Q0 >> Q) & (Q1 >> Q)
                        for k in range(2):
                            q_timer_clauses.append([-var(t=f'Q{k}', pos=(i, j), turn=turn),
                                                    var(t='Q', pos=(i, j), turn=turn)])

                        # not both Q0 and Q1
                        q_timer_clauses.append([-var(t=f'Q0', pos=(i, j), turn=turn),
                                                -var(t=f'Q1', pos=(i, j), turn=turn)])

                        if turn + 1 < num_turns:
                            # If Q1 at turn x --> Q0 at turn x+1
                            q_timer_clauses.append([-var(t='Q1', pos=(i, j), turn=turn), var('Q0', pos=(i, j), turn=turn + 1)])
                            # If Q0 at turn x --> H at turn x+1
                            q_timer_clauses.append([-var(t='Q0', pos=(i, j), turn=turn), var('H', pos=(i, j), turn=turn + 1)])
                            # If Q0 at turn x+1 --> Q1 at turn x
                            q_timer_clauses.append([-var(t='Q0', pos=(i, j), turn=turn + 1), var('Q1', pos=(i, j), turn=turn)])
                            # If Q1 at turn x+1 --> S at turn x
                            clauses.append([-var('Q1', pos=(i, j), turn=turn + 1), var('S', pos=(i, j), turn=turn)])

                        clauses.extend(q_timer_clauses)

                if turn + 1 < num_turns:

                    # If I at turn x --> I at turn x+1
                    if num_medics > 0:
                        clauses.append([-var('I', pos=(i, j), turn=turn), var('I', pos=(i, j), turn=turn + 1)])

                    # if H at turn x --> I or S or H at turn x+1
                    if num_medics > 0:
                        clauses.append([-var('H', pos=(i, j), turn=turn), var('I', pos=(i, j), turn=turn + 1),
                                        var('S', pos=(i, j), turn=turn + 1), var('H', pos=(i, j), turn=turn + 1)])
                    else:
                        clauses.append([-var('H', pos=(i, j), turn=turn), var('S', pos=(i, j), turn=turn + 1),
                                        var('H', pos=(i, j), turn=turn + 1)])

                    add_H_clause = True
                    if add_H_clause:
                        """ H_clause
                        if H and all neighbours not S (after bound checking) at turn x --> H or I (if num medcis >0) at turn x+1
                        to_cnf((a & (b &c&d&e)) >> (f|g), simplify=True)
                        f∨g∨¬a∨¬b∨¬c∨¬d∨¬e
                        """
                        H_clause = [-var('H', pos=(i, j), turn=turn), var('H', pos=(i, j), turn=turn + 1)]
                        if num_medics > 0:
                            H_clause.append(var('I', pos=(i, j), turn=turn + 1))

                        if i + 1 < num_rows:
                            H_clause.append(var('S', pos=(i + 1, j), turn=turn))

                        if i - 1 >= 0:
                            H_clause.append(var('S', pos=(i - 1, j), turn=turn))

                        if j + 1 < num_cols:
                            H_clause.append(var('S', pos=(i, j + 1), turn=turn))

                        if j - 1 >= 0:
                            H_clause.append(var('S', pos=(i, j - 1), turn=turn))
                        clauses.append(H_clause)

                    add_S_clauses = True
                    if add_S_clauses:
                        """ S_clauses
                        if num police = 0 and num medic =0:
                        If H and atleast one neighbour is S at turn x --> S2 at turn x+1
                        
                        if num police > 0 and num medic =0:
                            If H and atleast one neighbour is S at turn x --> S2 or H at turn x+1
                        
                        if num police = 0 and num medic >0:
                            If H and atleast one neighbour is S at turn x --> S2 or I at turn x+1
                        
                        if num police > 0 and num medic >0:
                            If H and atleast one neighbour is S at turn x --> S2 or H or I at turn x+1
                         
                         to_cnf(((a & (b |c|d))) >> (f|g), simplify=True)
                            (f∨g∨¬a∨¬b)∧(f∨g∨¬a∨¬c)∧(f∨g∨¬a∨¬d)
                        """
                        S_clauses = []
                        if i + 1 < num_rows:
                            prop1 = [-var('S', pos=(i + 1, j), turn=turn), -var('H', pos=(i, j), turn=turn),
                                     var('S2', pos=(i, j), turn=turn + 1)]
                            if num_medics > 0:
                                prop1.append(var('I', pos=(i, j), turn=turn + 1))
                            if num_police > 0:
                                prop1.append(var('H', pos=(i, j), turn=turn + 1))
                            S_clauses.append(prop1)

                        if i - 1 >= 0:
                            prop2 = [-var('S', pos=(i - 1, j), turn=turn),
                                     -var('H', pos=(i, j), turn=turn),
                                     var('S2', pos=(i, j), turn=turn + 1)]
                            if num_medics > 0:
                                prop2.append(var('I', pos=(i, j), turn=turn + 1))
                            if num_police > 0:
                                prop2.append(var('H', pos=(i, j), turn=turn + 1))
                            S_clauses.append(prop2)

                        if j + 1 < num_cols:
                            prop3 = [-var('S', pos=(i, j + 1), turn=turn),
                                     -var('H', pos=(i, j), turn=turn),
                                     var('S2', pos=(i, j), turn=turn + 1)]
                            if num_medics > 0:
                                prop3.append(var('I', pos=(i, j), turn=turn + 1))
                            if num_police > 0:
                                prop3.append(var('H', pos=(i, j), turn=turn + 1))
                            S_clauses.append(prop3)

                        if j - 1 >= 0:
                            prop4 = [-var('S', pos=(i, j - 1), turn=turn),
                                     -var('H', pos=(i, j), turn=turn),
                                     var('S2', pos=(i, j), turn=turn + 1)]
                            if num_medics > 0:
                                prop4.append(var('I', pos=(i, j), turn=turn + 1))
                            if num_police > 0:
                                prop4.append(var('H', pos=(i, j), turn=turn + 1))
                            S_clauses.append(prop4)

                        clauses.extend(S_clauses)

                    add_h_then_h_clause = True
                    if add_h_then_h_clause:
                        # If H at turn x and H at turn x+1 --> each neighbour in turn x was:
                        #                                                   1. not S
                        #                                                   or
                        #                                                   2. was S at turn x
                        #                                                       but Q at turn x+1
                        h_then_h_clause = [-var('H', pos=(i, j), turn=turn),
                                           -var('H', pos=(i, j), turn=turn + 1)]

                        if i + 1 < num_rows:
                            h_then_h_clause_d = h_then_h_clause.copy()
                            h_then_h_clause_d.append(-var('S', pos=(i + 1, j), turn=turn))
                            if num_police > 0:
                                h_then_h_clause_d.append(var('Q', pos=(i + 1, j), turn=turn + 1))
                            clauses.append(h_then_h_clause_d)

                        if i - 1 >= 0:
                            h_then_h_clause_u = h_then_h_clause.copy()
                            h_then_h_clause_u.append(-var('S', pos=(i - 1, j), turn=turn))
                            if num_police > 0:
                                h_then_h_clause_u.append(var('Q', pos=(i - 1, j), turn=turn + 1))
                            clauses.append(h_then_h_clause_u)

                        if j + 1 < num_cols:
                            h_then_h_clause_r = h_then_h_clause.copy()
                            h_then_h_clause_r.append(-var('S', pos=(i, j + 1), turn=turn))
                            if num_police > 0:
                                h_then_h_clause_r.append(var('Q', pos=(i, j + 1), turn=turn + 1))
                            clauses.append(h_then_h_clause_r)

                        if j - 1 >= 0:
                            h_then_h_clause_l = h_then_h_clause.copy()
                            h_then_h_clause_l.append(-var('S', pos=(i, j - 1), turn=turn))
                            if num_police > 0:
                                h_then_h_clause_l.append(var('Q', pos=(i, j - 1), turn=turn + 1))
                            clauses.append(h_then_h_clause_l)

        if turn > 0:  # because there are no Q or I at turn 0

            if num_medics >= 1 or num_police >= 1:
                # parse map for ? (of different types) and Q cells
                unk_was_unk = []
                unk_was_not_s_nor_unk = []
                unk_was_s = []
                unk_was_s_or_unk = []
                it_is_q = []
                it_is_i = []
                unk_was_h = []
                new_i = []
                was_h = []
                was_s = []
                unk_was_not_h_nor_unk = []
                for i, row in enumerate(obs):
                    for j, cell_type in enumerate(row):
                        cell_in_prev_turn = observations[turn - 1][i][j]
                        if cell_type == '?':
                            if cell_in_prev_turn == 'S':
                                unk_was_s.append((cell_type, (i, j)))
                            elif cell_in_prev_turn == '?':
                                unk_was_unk.append((cell_type, (i, j)))
                            else:
                                unk_was_not_s_nor_unk.append((cell_type, (i, j)))

                            if cell_in_prev_turn == 'H':
                                unk_was_h.append((cell_type, (i, j)))
                            elif cell_in_prev_turn != '?':
                                unk_was_not_h_nor_unk.append((cell_type, (i, j)))

                        elif cell_type == 'Q':
                            it_is_q.append((cell_type, (i, j)))
                        elif cell_type == 'I':
                            it_is_i.append((cell_type, (i, j)))
                            if cell_in_prev_turn == 'H':
                                new_i.append((cell_type, (i, j)))
                        if cell_in_prev_turn == 'H':
                            was_h.append((cell_type, (i, j)))
                        elif cell_in_prev_turn == 'S':
                            was_s.append((cell_type, (i, j)))

                unk_was_s_or_unk.extend(unk_was_s)
                unk_was_s_or_unk.extend(unk_was_unk)

                if num_police >= 1:
                    # if Q and was S before, or if Q and is Q next turn
                    num_certain_q1 = 0
                    for i, row in enumerate(obs):
                        for j, cell_type in enumerate(row):
                            cell_in_prev_turn = observations[turn - 1][i][j]
                            if cell_type == 'Q' and \
                                    (cell_in_prev_turn == 'S' or
                                     (turn + 1 < num_turns and observations[turn + 1][i][j] == 'Q')):
                                num_certain_q1 += 1
                    num_possible_q = num_police - num_certain_q1

                    # Q1 at turn x --> S or ? at turn x-1
                    # Can be Q1 only if was S or ? the turn before
                    #  Translated to if it was not S and not ? then it is not Q1
                    for cell_type, cell_pos in unk_was_not_s_nor_unk:
                        clauses.append([-var('Q1', pos=cell_pos, turn=turn)])

                    # at most num_police cells are Q1 at this turn, among all cells that are Q or ?(that where S or ?)
                    possible_q1 = it_is_q.copy()
                    possible_q1.extend(unk_was_s_or_unk)
                    atmost_q1_among_possible_q = CardEnc.atmost(lits=[var('Q1', pos=(i, j), turn=turn) for _, (i, j) in possible_q1],
                                                                vpool=vpool,
                                                                bound=num_police).clauses
                    cnf.extend(atmost_q1_among_possible_q)

                    # If we didn't see the Q1 then any one of ? can be Q1 aka atmost num_possible_q is Q1.
                    atmost_q1_among_unk = CardEnc.atmost(lits=[var('Q1', pos=(i, j), turn=turn) for _, (i, j) in unk_was_s_or_unk],
                                                         vpool=vpool, bound=num_possible_q).clauses

                    cnf.extend(atmost_q1_among_unk)

                    # at turn x: atleast K cells are Q1 among all map - Q or (? that were (S or ?))t
                    # K = min(num_police, #S prev turn)
                    # possible_q1_among_unk = ? that was S or ?
                    num_atleast_possible_q = min(num_police, len(was_s))
                    atleast_q1 = CardEnc.atleast(lits=[var('Q1', pos=(i, j), turn=turn) for _, (i, j) in possible_q1],
                                                 vpool=vpool, bound=num_atleast_possible_q).clauses

                    cnf.extend(atleast_q1)

                    # at turn x: atleast K cells become Q1 among possible_q1_among_unk
                    # K = min(max(num_police - #new_Q1 - #is?was?, 0), #?_was_S)
                    # possible_i_among_unk = ? that was H or ?
                    possible_q1_among_unk = unk_was_s_or_unk
                    num_atleast_q1_among_unk = min(max(num_police-num_certain_q1-len(unk_was_unk), 0), len(unk_was_s))
                    atleast_q1_among_unk_clause = CardEnc.atleast(lits=[var('I', pos=(i, j), turn=turn) for _, (i, j) in possible_q1_among_unk],
                                                       vpool=vpool,
                                                       bound=num_atleast_q1_among_unk).clauses

                    cnf.extend(atleast_q1_among_unk_clause)

                if num_medics >= 1:
                    # at most turn*num_medics among possible_i_among_unk can be I
                    # possible_i_among_unk is ? that was H or ?
                    num_known_i = len(it_is_i)
                    num_new_i = len(new_i)

                    possible_i_among_unk = unk_was_unk.copy()
                    possible_i_among_unk.extend(unk_was_h)
                    num_atmost_i_among_unk = min(num_medics * turn - num_known_i, num_medics - num_new_i)

                    atmost_i_among_possible_i = CardEnc.atmost(lits=[var('I', pos=(i, j), turn=turn) for _, (i, j) in possible_i_among_unk],
                                                               vpool=vpool,
                                                               bound=num_atmost_i_among_unk).clauses

                    cnf.extend(atmost_i_among_possible_i)

                    # all other ? cannot be I
                    for cell_type, cell_pos in unk_was_not_h_nor_unk:
                        clauses.append([-var('I', pos=cell_pos, turn=turn)])

                    # at turn x: atleast K cells are I among all map (possible_i_among_unk and I)
                    # K = min(num_medics * x, #H prev turn)
                    # possible_i_among_unk = ? that was H or ?
                    num_atleast_i = min(num_medics * turn, len(was_h))
                    possible_i = possible_i_among_unk.copy()
                    possible_i.extend(it_is_i)
                    atleast_i_clause = CardEnc.atleast(lits=[var('I', pos=(i, j), turn=turn) for _, (i, j) in possible_i],
                                                       vpool=vpool,
                                                       bound=num_atleast_i).clauses

                    cnf.extend(atleast_i_clause)

                    # at turn x: atleast K cells become I among possible_i_among_unk
                    # K = min(max(num_medics - #new_I - #is?was?, 0), #?_was_h)
                    # possible_i_among_unk = ? that was H or ?
                    num_atleast_i_among_unk = min(max(num_medics-len(new_i)-len(unk_was_unk), 0), len(unk_was_h))
                    atleast_i_among_unk_clause = CardEnc.atleast(lits=[var('I', pos=(i, j), turn=turn) for _, (i, j) in possible_i_among_unk],
                                                       vpool=vpool,
                                                       bound=num_atleast_i_among_unk).clauses

                    cnf.extend(atleast_i_among_unk_clause)

    cnf.extend(clauses)
    return cnf