示例#1
0
    def __init__(self, alphabet: Set[str], oracle: Oracle):
        """
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        :param oracle: Minimally adequate teacher (MAT)
        :type oracle: Oracle
        """
        super().__init__(alphabet, oracle)

        self._logger = Logger().get_logger()
        self._oracle = oracle
        self._red = set()
        self._blue = set()

        self._logger.info('Created Active Learner [LSTAR] instance with {} oracle'
                          .format('Active' if type(oracle) is ActiveOracle else 'Passive'))
示例#2
0
    def __init__(self, alphabet: Set[str], oracle: Oracle):
        """
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        :param oracle: Minimally adequate teacher (MAT)
        :type oracle: Oracle
        """
        super().__init__(alphabet, oracle)

        self._logger = Logger().get_logger()
        self._logger.info(
            'Created Active Learner [NLSTAR] instance with {} oracle'.format(
                'Active' if type(oracle) is ActiveOracle else 'Passive'))

        self._oracle = oracle

        self._logger.info('Initialising the table.')
        self._ot = ObservationTable(self._alphabet, oracle)
        self._hypothesis = None
示例#3
0
文件: gold.py 项目: vumaiha/inferrer
    def __init__(self, pos_examples: Set[str], neg_examples: Set[str], alphabet: Set[str]):
        """
        :param pos_examples: Set of positive example strings
                             from the target language
        :type pos_examples: Set[str]
        :param neg_examples: Set of negative example strings,
                             i.e strings that do not belong in
                             the target language.
        :type neg_examples: Set[str]
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        """
        super().__init__(alphabet, pos_examples, neg_examples)

        self._logger = Logger().get_logger()
        self._samples = pos_examples.union(neg_examples)

        self._red = {''}
        self._blue = set()

        self._logger.info('Created Passive Learner [Gold] instance')
示例#4
0
class LSTAR(ActiveLearner):
    """
    An implementation of Dana Angluin's L* algorithm, which
    learns regular languages from queries and counterexamples.

    The general idea of L* is to:
    Find a consistent observation table (representing a DFA).
    Submit is as an equivalence query.
    Use the counter-example to update the table.
    Submit membership queries to make the table closed and complete.
    Iterate until the Oracle tells us the correct language has been
    reached.
    """

    def __init__(self, alphabet: Set[str], oracle: Oracle):
        """
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        :param oracle: Minimally adequate teacher (MAT)
        :type oracle: Oracle
        """
        super().__init__(alphabet, oracle)

        self._logger = Logger().get_logger()
        self._oracle = oracle
        self._red = set()
        self._blue = set()

        self._logger.info('Created Active Learner [LSTAR] instance with {} oracle'
                          .format('Active' if type(oracle) is ActiveOracle else 'Passive'))

    def learn(self) -> automaton.DFA:
        """
        Efficiently learns an initially unknown regular language
        from a minimally adequate Teacher (Oracle).

        :return: The dfa accepting the target language.
        :rtype: DFA
        """
        self._logger.info('Start learning.')
        ot = self._initialise()

        while True:
            is_closed, is_consistent = ot.is_closed_and_consistent()
            while not is_closed or not is_consistent:
                if not is_closed:
                    ot = self._close(ot)

                if not is_consistent:
                    ot = self._consistent(ot)

                is_closed, is_consistent = ot.is_closed_and_consistent()

            dfa = self._build_automaton(ot)
            self._logger.info('Submitting equivalence query.')
            answer, satisfied = self._oracle.equivalence_query(dfa)

            if satisfied:
                self._logger.info('Oracle happy with our hypothesis.')
                break

            self._logger.info('Oracle return {} as counterexample.'.format(answer))
            ot = self._useq(ot, answer)

        return dfa

    def _initialise(self) -> utils.ObservationTable:
        """
        Initialises an observation table. This consists of
        building one red row and as many blue rows as there
        are symbols in the alphabet.

        :return: Initialised observation table
        :rtype: ObservationTable
        """
        self._logger.info('Initialising the table.')
        self._red = {''}
        self._blue = copy.deepcopy(self._alphabet)

        ot = utils.ObservationTable(self._blue, self._red, self._alphabet)

        ot.sta = self._red.union(self._blue)
        ot.exp = {''}

        ot.put('', '', self._oracle.membership_query(''))
        for a in self._alphabet:
            ot.put(a, '', self._oracle.membership_query(a))

        return ot

    def _close(self, ot: utils.ObservationTable) -> utils.ObservationTable:
        """
        Closes the observation table by adding an extra row.

        :param ot: The observation table to close.
        :type ot: ObservationTable
        :return: The closed and updated observation table
        :rtype: ObservationTable
        """
        self._logger.info('Closing the table by adding a row.')
        for s in self._blue.copy():
            if not all([ot.get_row(s) != ot.get_row(u) for u in self._red]):
                continue

            self._red.add(s)
            self._blue.remove(s)

            for a in self._alphabet:
                sa = s + a
                if sa not in self._blue:
                    self._blue.add(sa)
                    ot.add_row(sa)

            for u, e in ot.find_holes():
                ot.put(u, e, self._oracle.membership_query(u + e))

        return ot

    def _consistent(self, ot: utils.ObservationTable) -> utils.ObservationTable:
        """
        Makes the observation table consistent by adding an extra
        column.

        :param ot: The observation table to make consistent.
        :type ot: ObservationTable
        :return: The consistent and updated observation table
        :rtype: ObservationTable
        """
        self._logger.info('Making the table consistent by adding a column.')
        s1, s2, a, e = self._find_inconsistent(ot)

        ae = a + e
        ot.exp.add(ae)
        ot.add_column_to_table(ae)

        for u, e in ot.find_holes():
            ot.put(u, e, self._oracle.membership_query(u + e))

        return ot

    def _find_inconsistent(self, ot: utils.ObservationTable) -> Tuple[str, str, str, str]:
        """
        Tries to find two inconsistent rows s1 and s2 in the
        observation table. s1 and s2 are elements of red.
        OT[s1] == OT[s2] and OT[s1.a][e] != OT[s2.a][e] where
        a is an element in the alphabet and e is an
        experiment (element in the set ot.exp)

        :param ot: The observation table to find two inconsistent
                   red states.
        :type ot: ObservationTable
        :return: Inconsistent row
        :rtype: Tuple[str, str, str, str]
        """
        self._logger.info('Trying to find two inconsistent rows in the table.')
        for s1 in self._red:
            for s2 in self._red:
                if s1 == s2:
                    continue
                for a in self._alphabet:
                    for e in ot.exp:
                        if ot.get_row(s1) == ot.get_row(s2) and \
                            ot.entry_exists(s1 + a, e) and ot.entry_exists(s2 + a, e) \
                                and ot.get(s1 + a, e) != ot.get(s2 + a, e):
                            self._logger.info('Found two inconsistent rows {} and {}'
                                              .format(s1, s2))
                            return s1, s2, a, e

        self._logger.info('Did not find a inconsistency in the table.')
        return '', '', '', ''

    def _useq(self, ot: utils.ObservationTable, answer: str) -> utils.ObservationTable:
        """
        This method is called when the table is closed and complete.
        The algorithm then makes an equivalence query to the oracle,
        if the oracle is not satisfied and provides us with a
        counterexample, then this method is called with that counterexample.
        The method adds new rows to the observation table, to account
        for the new counterexample.

        :param ot: The observation table to update
        :type ot: ObservationTable
        :param answer: The counter-example given by the oracle
        :type answer: str
        :return: Updated ObservationTable
        :rtype: ObservationTable
        """
        prefix_set = set(utils.prefix_set({answer}, self._alphabet))
        self._logger.info('Updating table by adding the following prefixes: {}'
                          .format(', '.join(prefix_set)))

        for p in prefix_set:

            if p not in self._red:
                if p not in self._blue:
                    ot.add_row(p)
                self._red.add(p)
                self._blue.discard(p)

            for a in self._alphabet:
                pa = p + a
                if pa not in prefix_set:
                    if pa not in self._blue:
                        if pa not in self._red:
                            ot.add_row(pa)
                        self._blue.add(pa)
                        self._red.discard(pa)

        for u, e in ot.find_holes():
            ot.put(u, e, self._oracle.membership_query(u + e))

        return ot

    def _build_automaton(self, ot: utils.ObservationTable) -> automaton.DFA:
        """
        Builds an automaton from the observation table.

        :param ot: The data to build the dfa from.
        :type ot: ObservationTable
        :return: The dfa built from the observation table.
        :rtype: DFA
        """
        self._logger.info('Building DFA from the table.')
        dfa = automaton.DFA(self._alphabet)

        for u in self._red:
            for v in ot.ot.keys():
                if u == v:
                    continue

                if len(v) < len(u) and ot.get_row(v) != ot.get_row(u):
                    dfa.states.add(automaton.State(u))

        for u in dfa.states:
            if ot.entry_exists(u.name, ''):
                if ot.get(u.name, '') == 1:
                    dfa.accept_states.add(u)
                elif ot.get(u.name, '') == 0:
                    dfa.reject_states.add(u)

            for a in self._alphabet:
                for w in dfa.states:
                    if ot.get_row(u.name + a) == ot.get_row(w.name):
                        dfa.add_transition(u, w, a)

        return dfa.rename_states()
示例#5
0
class NLSTAR(ActiveLearner):
    """
    An implementation of the NL* algorithm, which extends
    Angluin-Style learning to the learning of an NFA.

    The general idea of NL* is to:
    Find a consistent observation table (representing a RFSA).
    Submit is as an equivalence query.
    Use the counter-example to update the table.
    Submit membership queries to make the table closed and complete.
    Iterate until the Oracle tells us the correct language has been
    reached.
    """
    def __init__(self, alphabet: Set[str], oracle: Oracle):
        """
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        :param oracle: Minimally adequate teacher (MAT)
        :type oracle: Oracle
        """
        super().__init__(alphabet, oracle)

        self._logger = Logger().get_logger()
        self._logger.info(
            'Created Active Learner [NLSTAR] instance with {} oracle'.format(
                'Active' if type(oracle) is ActiveOracle else 'Passive'))

        self._oracle = oracle

        self._logger.info('Initialising the table.')
        self._ot = ObservationTable(self._alphabet, oracle)
        self._hypothesis = None

    def learn(self) -> NFA:
        """
        Infers an initially unknown regular language
        from a minimally adequate Teacher (Oracle).

        :return: The nfa accepting the target language.
        :rtype: NFA
        """
        self._logger.info('Start learning.')
        self._ot.initialize()

        while True:
            closed_info, consistency_info = self._ot.is_closed_and_consistent()

            is_closed, unclosed = closed_info
            is_consistent, sym, suffix = consistency_info

            while not is_closed or not is_consistent:
                if not is_closed:
                    self._close_table(unclosed)

                if not is_consistent:
                    self._make_table_consistent(sym, suffix)

                closed_info, consistency_info = self._ot.is_closed_and_consistent(
                )

                is_closed, unclosed = closed_info
                is_consistent, sym, suffix = consistency_info

            hypothesis = self._build_hypothesis()

            self._logger.info('Submitting equivalence query.')
            answer, satisfied = self._oracle.equivalence_query(hypothesis)

            if satisfied:
                self._logger.info('Oracle happy with our hypothesis.')
                break

            self._logger.info(
                'Oracle return {} as counterexample.'.format(answer))
            self._use_eq(answer)

        return hypothesis

    def _use_eq(self, eq: str):
        """
        When the Oracle is not happy with our
        hypothesis and returns a counterexample,
        this method adds the new suffixes to the
        observation table and updates all the rows.

        :param eq: Counterexample returned
                   by the oracle.
        :type eq: str
        """
        new_suffixes = {eq[i:] for i in range(len(eq) + 1)}

        self._logger.info(
            'Updating table by adding the following suffixes: {}'.format(
                ', '.join(new_suffixes)))

        self._ot.add_new_suffixes(new_suffixes)
        self._ot.update_meta_data()

    def _close_table(self, unclosed_row):
        """
        Attempts to close the observation table
        by adding a new row to the table.
        """
        self._logger.info('Attempting to close the table by adding a new row.')

        self._ot.upper_rows.add(unclosed_row)
        self._ot.lower_rows.remove(unclosed_row)
        self._ot.upper_primes.add(unclosed_row)

        for symbol in self._alphabet:
            new_row = Row(unclosed_row.prefix + symbol)

            self._ot.rows.add(new_row)
            self._ot.lower_rows.add(new_row)
            self._ot.prefix_to_row[new_row.prefix] = new_row

            self._ot.add_columns_to_row(new_row)

        self._ot.update_meta_data()

    def _make_table_consistent(self, sym, suffix):
        """
        Attempts to make the observation table
        consistent by adding a new column (experiment)
        to the table.
        """
        self._logger.info(
            'Attempting to make the table consistent by adding a new column.')

        new_suffix = '{}{}'.format(sym, suffix)
        self._ot.add_suffix(new_suffix)
        self._ot.update_meta_data()

    def _build_hypothesis(self) -> NFA:
        """
        Builds a NFA from the observation table,
        which we will use when making an
        equivalence query to the Oracle.

        :return: The "hypothesis" NFA.
        :rtype: NFA
        """
        self._logger.info('Building NFA from the table.')
        nfa = NFA(self._alphabet)
        epsilon_row = self._ot.get_epsilon_row()

        for row in self._ot.upper_primes:
            state = State(row.prefix)
            nfa.add_state(state)

            if row.covered_by(epsilon_row):
                nfa.add_start_state(state)

            if row.columns['']:
                nfa.add_accepting_state(state)

        for u in self._ot.rows:
            state = State(u.prefix)
            if state not in nfa.get_states():
                continue

            for a in self._alphabet:
                ua_row = self._ot.get_row_by_prefix(u.prefix + a)

                if ua_row is None:
                    continue

                for r in nfa.get_states():
                    row = self._ot.get_row_by_prefix(r.name)
                    if row.covered_by(ua_row):
                        nfa.add_transition(state, r, a)

        return nfa.rename_states()
示例#6
0
class Gold(PassiveLearner):
    """
    An implementation of E. Mark GOLD's algorithm, which tries
    to find the minimum DFA consistent with the sample.
    """
    def __init__(self, pos_examples: Set[str], neg_examples: Set[str],
                 alphabet: Set[str]):
        """
        :param pos_examples: Set of positive example strings
                             from the target language
        :type pos_examples: Set[str]
        :param neg_examples: Set of negative example strings,
                             i.e strings that do not belong in
                             the target language.
        :type neg_examples: Set[str]
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        """
        super().__init__(alphabet, pos_examples, neg_examples)

        self._logger = Logger().get_logger()
        self._samples = pos_examples.union(neg_examples)

        self._red = {''}
        self._blue = set()

        self._logger.info('Created Passive Learner [Gold] instance')

    def learn(self) -> automaton.DFA:
        """
        Learns the grammar from the sets of positive and negative
        example strings. This method returns the minimal DFA
        consistent with the sample.

        :return: The minimum DFA consistent with the sample
        :rtype: Automaton
        """
        self._logger.info('Start learning with alphabet = {}\n'
                          'positive samples = {}\n'
                          'negative samples = {}'.format(
                              self._alphabet, self._pos_examples,
                              self._neg_examples))
        ot = self._build_table()

        od_row, x = ot.obviously_different_row()
        while od_row:
            self._logger.info(
                'Processing obviously different row: {}.'.format(x))
            xa = [x + a for a in self._alphabet]
            ot.sta.update(xa)

            self._blue.update(xa)
            self._blue.discard(x)

            for u in ot.sta:
                for e in ot.exp:
                    ue = u + e
                    if ue in self._pos_examples:
                        ot.put(u, e, 1)
                    elif ue in self._neg_examples:
                        ot.put(u, e, 0)
                    else:
                        ot.put(u, e, None)

            od_row, x = ot.obviously_different_row()

        ot, failed = self._fill_holes(ot)

        if failed:
            self._logger.info('Failed to make table complete.')
            return automaton.build_pta(self._pos_examples, self._neg_examples)
        else:
            self._logger.info('Successfully completed table.')
            a = self._build_automaton(ot)

            if self._is_consistent(a, ot):
                self._logger.info(
                    'DFA and table is consistent, returning DFA.')
                return a.remove_dead_states()
            else:
                self._logger.info(
                    'DFA and table is not consistent, building PTA from samples.'
                )
                return automaton.build_pta(self._pos_examples,
                                           self._neg_examples)

    def _build_table(self) -> utils.ObservationTable:
        """
        Obtains a table from the sample.

        :return: Initial observation table
        :rtype: ObservationTable
        """
        self._logger.info('Building table from sample.')
        sta = {''}

        self._blue = self._alphabet.copy()

        exp = set(utils.suffix_set(self._samples, self._alphabet))

        ot = utils.ObservationTable(self._blue, self._red, self._alphabet)

        for p in self._red.union(self._blue):
            for e in exp:
                pe = p + e
                if pe in self._pos_examples:
                    ot.put(p, e, 1)
                elif pe in self._neg_examples:
                    ot.put(p, e, 0)
                else:
                    ot.put(p, e, None)

            sta.add(p)

        ot.sta = sta
        ot.exp = exp

        return ot

    def _fill_holes(
            self,
            ot: utils.ObservationTable) -> Tuple[utils.ObservationTable, bool]:
        """
        Tries to make the table complete by filling in all the entries that
        are None.

        :param ot: the updated observation table
        :return: updated ObservationTable and whether the method was successful.
        :rtype: tuple(ObservationTable, bool)
        """
        self._logger.info(
            'Try to make table complete by filling in * entries.')
        for p in self._blue:
            r = ot.find_compatible_row(p)
            if r is not None:
                for e in ot.exp:
                    if ot.entry_exists(p, e) and ot.get(p, e) is not None:
                        ot.put(r, e, ot.get(p, e))
            else:
                return ot, True

        for r in self._red:
            for e in ot.exp:
                if ot.entry_exists(r, e) and ot.get(r, e) is None:
                    ot.put(r, e, 1)

        for p in self._blue:
            r = ot.find_compatible_row(p)
            if r is not None:
                for e in ot.exp:
                    if ot.entry_exists(p, e) and ot.get(p, e) is None:
                        if ot.entry_exists(r, e):
                            ot.put(p, e, ot.get(r, e))
            else:
                return ot, True

        return ot, False

    def _build_automaton(self, ot: utils.ObservationTable) -> automaton.DFA:
        """
        Builds an automaton from the observation table.

        :type ot: ObservationTable
        :return: Automaton built from the observation table
        :rtype: Automaton
        """
        dfa = automaton.DFA(self._alphabet)

        states = {automaton.State(i) for i in self._red}

        we = utils.break_strings_in_two(self._red)
        for w, e in we:
            we = w + e
            if we in self._red and ot.entry_exists(w, e):
                val = ot.get(w, e)
                state = automaton.State(we)
                if val == 1:
                    dfa.accept_states.add(state)
                    states.add(state)
                elif val == 0:
                    dfa.reject_states.add(state)
                    states.add(state)

        for w in states:
            for a in self._alphabet:
                for u in self._red:
                    wa = w.name + a
                    if ot.row_exists(u) and ot.row_exists(wa) and \
                            ot.get_row(u) == ot.get_row(wa):
                        dfa.add_transition(w, automaton.State(u), a)

        return dfa

    def _is_consistent(self, dfa: automaton.DFA,
                       ot: utils.ObservationTable) -> bool:
        """
        Determines whether the automaton is consistent with the
        observation table ot.

        :type dfa: Automaton
        :type ot: ObservationTable
        :return: Whether the automaton and observation table are consistent.
        :rtype: bool
        """
        self._logger.info(
            'Determine whether the DFA is consistent with the table.')
        for u, col in ot.ot.items():
            for e, val in col.items():
                ue = automaton.State(u + e)
                if val == 1:
                    if ue not in dfa.accept_states:
                        return False
                elif val == 0:
                    if ue not in dfa.reject_states:
                        return False
        return True
示例#7
0
文件: rpni.py 项目: vumaiha/inferrer
class RPNI(PassiveLearner):
    """
    An implementation of the Regular Positive and Negative Inference (RPNI)
    algorithm. This algorithm tries to make sure that some generalisation
    takes place and, in the best case, returns the correct target automaton.
    """
    def __init__(self, pos_examples: Set[str], neg_examples: Set[str],
                 alphabet: Set[str]):
        """
        :param pos_examples: Set of positive example strings
                             from the target language
        :type pos_examples: Set[str]
        :param neg_examples: Set of negative example strings,
                             i.e strings that do not belong in
                             the target language.
        :type neg_examples: Set[str]
        :param alphabet: The alphabet (Sigma) of the target
                         regular language.
        :type alphabet: Set[str]
        """
        super().__init__(alphabet, pos_examples, neg_examples)

        self._logger = Logger().get_logger()
        self._samples = pos_examples.union(neg_examples)

        self._red = {automaton.State('')}
        self._blue = set()

        self._logger.info('Created Passive Learner [RPNI] instance')

    def learn(self) -> automaton.DFA:
        """
        Learns the grammar from the sets of positive and negative
        example strings. This method returns a DFA that is
        consistent with the sample.

        :return: DFA
        :rtype: Automaton
        """
        self._logger.info('Start learning with alphabet = {}\n'
                          'positive samples = {}\n'
                          'negative samples = {}'.format(
                              self._alphabet, self._pos_examples,
                              self._neg_examples))

        self._logger.info('Building PTA')
        dfa = automaton.build_pta(self._pos_examples)

        pref_set = utils.prefix_set(self._pos_examples)
        self._blue = {
            automaton.State(i)
            for i in self._alphabet.intersection(pref_set)
        }

        while len(self._blue) != 0:
            qb = _choose(self._blue)
            self._blue.remove(qb)

            found = False
            for qr in sorted(self._red, key=functools.cmp_to_key(_cmp)):
                if self._compatible(self._merge(dfa.copy(), qr, qb)):
                    dfa = self._merge(dfa, qr, qb)
                    new_blue_states = set()
                    for q in self._red:
                        for a in self._alphabet:
                            if dfa.transition_exists(q, a) and \
                                    dfa.transition(q, a) not in self._red:
                                new_blue_states.add(dfa.transition(q, a))

                    self._blue.update(new_blue_states)
                    found = True

            if not found:
                dfa = self._promote(qb, dfa)

        for s in self._neg_examples:
            q, accepted = dfa.parse_string(s)
            if not accepted:
                dfa.reject_states.add(q)

        return dfa.remove_dead_states()

    def _promote(self, qu: automaton.State,
                 dfa: automaton.DFA) -> automaton.DFA:
        """
        Given a state blue state qu, this method promotes this state
        ro red and all the successors in the dfa. The method returns
        the updated dfa.

        :param qu: State with colour blue
        :type qu: State
        :param dfa: the dfa
        :type dfa: Automaton
        :return: Updated dfa
        :rtype: Automaton
        """
        self._logger.info('Promoting state {} from blue to red'.format(
            qu.name))
        self._red.add(qu)

        self._blue.update({
            dfa.transition(qu, a)
            for a in self._alphabet if dfa.transition_exists(qu, a)
        })
        self._blue.discard(qu)

        return dfa

    def _compatible(self, dfa: automaton.DFA) -> bool:
        """
        Determines whether the current automaton can parse any
        string in the set of negative example strings.
        Returns True if the current automaton cannot parse
        any string from the negative examples, returns False
        if some counter-example is accepted by the current
        automaton.

        :param dfa: the dfa
        :type dfa: Automaton
        :return: Boolean indicating whether the dfa is compatible.
        :rtype: bool
        """
        return not any(dfa.parse_string(w)[1] for w in self._neg_examples)

    def _merge(self, dfa: automaton.DFA, q: automaton.State,
               q_prime: automaton.State) -> automaton.DFA:
        """
        Takes as arguments a red state q and a blue state q'.
        The method first finds the unique pair (qf, a) such
        that q' = delta(qf, a).
        The method then redirects delta(qf, a) to q. After that
        the tree rooted in q' is folded into the rest of the DFA.
        The possible intermediate situations of non-determinism
        are dealt with during the recursive calls to fold.

        :param dfa: the automaton to update with a merge
        :type dfa: Automaton
        :param q: State from the red set
        :type q: State
        :param q_prime: State from the blue
        :type q_prime: State
        :return: updated Automaton
        :rtype: Automaton
        """
        self._logger.info('Merging the two states {} and {}'.format(
            q.name, q_prime.name))
        qf, a = dfa.find_transition_to_q(q_prime)

        if qf is None or a is None:
            return dfa

        dfa.add_transition(qf, q, a)

        return self._fold(dfa, q, q_prime)

    def _fold(self, dfa: automaton.DFA, q: automaton.State,
              q_prime: automaton.State) -> automaton.DFA:
        """
        Folds the tree rooted in q' into the rest of the DFA. The
        possible intermediate situations of non-determinism
        are dealt with during the recursive calls.

        :param dfa: the automaton to update with a folding of states
        :type dfa: Automaton
        :param q: State
        :type q: State
        :param q_prime: State to fold
        :type q_prime: State
        :return: updated Automaton
        :rtype: Automaton
        """
        self._logger.info('Folding the tree rooted in the state {}'.format(
            q_prime.name))
        if q_prime in dfa.accept_states:
            dfa.accept_states.add(q)

        for a in self._alphabet:
            if dfa.transition_exists(q_prime, a):
                if dfa.transition_exists(q, a):
                    dfa = self._fold(dfa, dfa.transition(q, a),
                                     dfa.transition(q_prime, a))
                else:
                    dfa.add_transition(q, dfa.transition(q_prime, a), a)

        return dfa