tools/android/dependency_analysis/count_cycles.py
def find_cycles_from_node(
    start_node: graph.Node,
    max_cycle_length: int,
    node_to_id: Dict[graph.Node, int],
) -> List[List[List[graph.Node]]]:
    """Finds all cycles starting at |start_node| in a subset of nodes.

    Only nodes with ID >= |start_node|'s ID will be considered. This ensures
    uniquely counting all cycles since this function is called on all nodes of
    the graph, one at a time in increasing order. Some justification: Consider
    cycle C with smallest node n. When this function is called on node n, C will
    be found since all nodes of C are >= n. After that call, C will never be
    found again since further calls are on nodes > n (n is removed from the
    search space).

    Cycles are found by recursively scanning all outbound nodes starting from
    |start_node|, up to a certain depth. Note this is the same idea, but is
    different from DFS since nodes can be visited more than once (to avoid
    missing cycles). An example of normal DFS (where nodes can only be visited
    once) missing cycles is in the following graph, starting at a:
    a <-> b <-> c
    ^           ^
    |           |
    +-----------+
    DFS(a)
        DFS(b)
            DFS(a) (cycle aba, return)
            DFS(c)
                DFS(b) (already seen, return)
                DFS(a) (cycle abca, return)
        DFS(c) (already seen, return)
    Since DFS(c) cannot proceed, we miss the cycles aca and acba.

    Args:
        start_node: The node to start the cycle search from. Only nodes with ID
          >= |start_node|'s ID will be considered.
        max_cycle_length: The maximum length of cycles to be found.
        node_to_id: A map from a Node to a generated ID.

    Returns:
        A list |cycles| of length |max_cycle_length| + 1, where cycles[i]
          contains all relevant cycles of length i.
    """
    start_node_id = node_to_id[start_node]
    cycles = [[] for _ in range(max_cycle_length + 1)]

    def edge_is_interesting(start: graph.Node, end: graph.Node) -> bool:
        if start == end:
            # Ignore self-loops.
            return False
        if node_to_id[end] < start_node_id:
            # Ignore edges ending at nodes with ID lower than the start.
            return False
        return True

    dfs_stack = collections.deque()
    on_stack: Dict[graph.Node, bool] = collections.defaultdict(bool)

    def find_cycles_dfs(cur_node: graph.Node, cur_length: int):
        for other_node in cur_node.outbound:
            if edge_is_interesting(cur_node, other_node):
                if other_node == start_node:
                    # We have found a valid cycle, add it to the list.
                    new_cycle = list(dfs_stack) + [cur_node, start_node]
                    cycles[cur_length + 1].append(new_cycle)

                elif (not on_stack[other_node]
                      and cur_length + 1 < max_cycle_length):
                    # We are only allowed to recurse into the next node if:
                    # 1) It hasn't been visited in the current cycle. This is
                    # because if the next node n _has_ been visited in the
                    # current cycle (i.e., it's on the stack), then we have
                    # found a cycle starting and ending at n. Since this
                    # function only returns cycles starting at |start_node|, we
                    # only care if |n = start_node| (which we already detect
                    # above).
                    # 2) It would not exceed the maximum depth allowed.
                    dfs_stack.append(cur_node)
                    on_stack[cur_node] = True
                    find_cycles_dfs(other_node, cur_length + 1)
                    dfs_stack.pop()
                    on_stack[cur_node] = False

    find_cycles_dfs(start_node, 0)
    return cycles
Finds all cycles starting at |start_node| in a subset of nodes.

    Only nodes with ID >= |start_node|'s ID will be considered. This ensures
    uniquely counting all cycles since this function is called on all nodes of
    the graph, one at a time in increasing order. Some justification: Consider
    cycle C with smallest node n. When this function is called on node n, C will
    be found since all nodes of C are >= n. After that call, C will never be
    found again since further calls are on nodes > n (n is removed from the
    search space).

    Cycles are found by recursively scanning all outbound nodes starting from
    |start_node|, up to a certain depth. Note this is the same idea, but is
    different from DFS since nodes can be visited more than once (to avoid
    missing cycles). An example of normal DFS (where nodes can only be visited
    once) missing cycles is in the following graph, starting at a:
    a <-> b <-> c
    ^           ^
    |           |
    +-----------+
    DFS(a)
        DFS(b)
            DFS(a) (cycle aba, return)
            DFS(c)
                DFS(b) (already seen, return)
                DFS(a) (cycle abca, return)
        DFS(c) (already seen, return)
    Since DFS(c) cannot proceed, we miss the cycles aca and acba.

    Args:
        start_node: The node to start the cycle search from. Only nodes with ID
          >= |start_node|'s ID will be considered.
        max_cycle_length: The maximum length of cycles to be found.
        node_to_id: A map from a Node to a generated ID.

    Returns:
        A list |cycles| of length |max_cycle_length| + 1, where cycles[i]
          contains all relevant cycles of length i.
    
Finds all cycles starting at |start_node| in a subset of nodes.
Only nodes with ID >= |start_node|'s ID will be considered. This ensures
uniquely counting all cycles since this function is called on all nodes of
the graph, one at a time in increasing order. Some justification: Consider
cycle C with smallest node n. When this function is called on node n, C will
be found since all nodes of C are >= n. After that call, C will never be
found again since further calls are on nodes > n (n is removed from the
search space).

Cycles are found by recursively scanning all outbound nodes starting from
|start_node|, up to a certain depth. Note this is the same idea, but is
different from DFS since nodes can be visited more than once (to avoid
missing cycles).
Finds all cycles starting at |start_node| in a subset of nodes . Only nodes with ID > = |start_node| ' s ID will be considered . This ensures uniquely counting all cycles since this function is called on all nodes of the graph one at a time in increasing order . Some justification : Consider cycle C with smallest node n . When this function is called on node n C will be found since all nodes of C are > = n . After that call C will never be found again since further calls are on nodes > n ( n is removed from the search space ) . Cycles are found by recursively scanning all outbound nodes starting from |start_node| up to a certain depth . Note this is the same idea but is different from DFS since nodes can be visited more than once ( to avoid missing cycles ) .
def find_cycles_from_node(
    start_node: graph.Node,
    max_cycle_length: int,
    node_to_id: Dict[graph.Node, int],
) -> List[List[List[graph.Node]]]:
    start_node_id = node_to_id[start_node]
    cycles = [[] for _ in range(max_cycle_length + 1)]
    def edge_is_interesting(start: graph.Node, end: graph.Node) -> bool:
        if start == end:
            return False
        if node_to_id[end] < start_node_id:
            return False
        return True
    dfs_stack = collections.deque()
    on_stack: Dict[graph.Node, bool] = collections.defaultdict(bool)
    def find_cycles_dfs(cur_node: graph.Node, cur_length: int):
        for other_node in cur_node.outbound:
            if edge_is_interesting(cur_node, other_node):
                if other_node == start_node:
                    new_cycle = list(dfs_stack) + [cur_node, start_node]
                    cycles[cur_length + 1].append(new_cycle)
                elif (not on_stack[other_node]
                      and cur_length + 1 < max_cycle_length):
                    dfs_stack.append(cur_node)
                    on_stack[cur_node] = True
                    find_cycles_dfs(other_node, cur_length + 1)
                    dfs_stack.pop()
                    on_stack[cur_node] = False
    find_cycles_dfs(start_node, 0)
    return cycles
def find_cycles_from_node ( start_node : graph . Node , max_cycle_length : int , node_to_id : Dict [ graph . Node , int ] , ) -> List [ List [ List [ graph . Node ] ] ] : start_node_id = node_to_id [ start_node ] cycles = [ [ ] for _ in range ( max_cycle_length + 1 ) ] def edge_is_interesting ( start : graph . Node , end : graph . Node ) -> bool : if start == end : return False if node_to_id [ end ] < start_node_id : return False return True dfs_stack = collections . deque ( ) on_stack : Dict [ graph . Node , bool ] = collections . defaultdict ( bool ) def find_cycles_dfs ( cur_node : graph . Node , cur_length : int ) : for other_node in cur_node . outbound : if edge_is_interesting ( cur_node , other_node ) : if other_node == start_node : new_cycle = list ( dfs_stack ) + [ cur_node , start_node ] cycles [ cur_length + 1 ] . append ( new_cycle ) elif ( not on_stack [ other_node ] and cur_length + 1 < max_cycle_length ) : dfs_stack . append ( cur_node ) on_stack [ cur_node ] = True find_cycles_dfs ( other_node , cur_length + 1 ) dfs_stack . pop ( ) on_stack [ cur_node ] = False find_cycles_dfs ( start_node , 0 ) return cycles
Finds all cycles starting at |start_node| in a subset of nodes.
Finds all cycles starting at |start_node| in a subset of nodes .
"""Finds all cycles starting at |start_node| in a subset of nodes.

    Only nodes with ID >= |start_node|'s ID will be considered. This ensures
    uniquely counting all cycles since this function is called on all nodes of
    the graph, one at a time in increasing order. Some justification: Consider
    cycle C with smallest node n. When this function is called on node n, C will
    be found since all nodes of C are >= n. After that call, C will never be
    found again since further calls are on nodes > n (n is removed from the
    search space).

    Cycles are found by recursively scanning all outbound nodes starting from
    |start_node|, up to a certain depth. Note this is the same idea, but is
    different from DFS since nodes can be visited more than once (to avoid
    missing cycles). An example of normal DFS (where nodes can only be visited
    once) missing cycles is in the following graph, starting at a:
    a <-> b <-> c
    ^           ^
    |           |
    +-----------+
    DFS(a)
        DFS(b)
            DFS(a) (cycle aba, return)
            DFS(c)
                DFS(b) (already seen, return)
                DFS(a) (cycle abca, return)
        DFS(c) (already seen, return)
    Since DFS(c) cannot proceed, we miss the cycles aca and acba.

    Args:
        start_node: The node to start the cycle search from. Only nodes with ID
          >= |start_node|'s ID will be considered.
        max_cycle_length: The maximum length of cycles to be found.
        node_to_id: A map from a Node to a generated ID.

    Returns:
        A list |cycles| of length |max_cycle_length| + 1, where cycles[i]
          contains all relevant cycles of length i.
    """ # Ignore self-loops. # Ignore edges ending at nodes with ID lower than the start. # We have found a valid cycle, add it to the list. # We are only allowed to recurse into the next node if: # 1) It hasn't been visited in the current cycle. This is # because if the next node n _has_ been visited in the # current cycle (i.e., it's on the stack), then we have # found a cycle starting and ending at n. Since this # function only returns cycles starting at |start_node|, we # only care if |n = start_node| (which we already detect # above). # 2) It would not exceed the maximum depth allowed.
A list |cycles| of length |max_cycle_length| + 1, where cycles[i]
contains all relevant cycles of length i.
A list |cycles| of length |max_cycle_length| + 1 where cycles [ i ] contains all relevant cycles of length i .
start_node
graph.Node
The node to start the cycle search from. Only nodes with ID
>= |start_node|'s ID will be considered.
The node to start the cycle search from . Only nodes with ID > = |start_node| ' s ID will be considered .
max_cycle_length
int
The maximum length of cycles to be found.
The maximum length of cycles to be found .
node_to_id
Dict[graph.Node, int]
A map from a Node to a generated ID.
A map from a Node to a generated ID .