Coverage for graphqler / chains / strategies / dfs_strategy.py: 37%
19 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
1"""DFS-based chain generation strategy."""
3import networkx
5from graphqler.chains.chain import Chain
6from graphqler.chains.strategies.base_strategy import BaseChainStrategy
7from graphqler.graph.node import Node
10class DFSChainStrategy(BaseChainStrategy):
11 """Generates chains via depth-first search from the starter nodes.
13 For every node visited during DFS, the current prefix path is recorded as a :class:`Chain`.
14 This means a path A -> B -> C produces three chains: [A], [A, B], [A, B, C].
16 Cycles are avoided by tracking the nodes already present in the current path.
18 Nodes whose ``mutation_type`` is in *filter_mutation_type* are excluded from chains;
19 DFS stops at those nodes and does not recurse into their subtrees.
20 """
22 def generate(self, graph: networkx.DiGraph, starter_nodes: list[Node],
23 filter_mutation_type: list[str] | None = None) -> list[Chain]:
24 """Run DFS from each starter node and collect all prefix chains.
26 Args:
27 graph (networkx.DiGraph): The dependency graph.
28 starter_nodes (list[Node]): Nodes to begin DFS from.
29 filter_mutation_type (list[str] | None): Mutation types to exclude.
30 Nodes whose ``mutation_type`` is in this list (and their entire subtrees)
31 will be skipped. Pass ``None`` or ``[]`` to include all nodes.
33 Returns:
34 list[Chain]: All prefix chains discovered during DFS.
35 """
36 excluded = set(filter_mutation_type) if filter_mutation_type else set()
37 chains: list[Chain] = []
38 for start_node in starter_nodes:
39 self._dfs(graph, start_node, [], chains, excluded)
40 return chains
42 def _dfs(self, graph: networkx.DiGraph, node: Node, current_path: list[Node],
43 chains: list[Chain], excluded: set[str]) -> None:
44 """Recursively performs DFS, appending a Chain for every prefix path.
46 If a node's ``mutation_type`` is in *excluded*, it and its entire subtree are skipped.
48 Args:
49 graph (networkx.DiGraph): The dependency graph.
50 node (Node): The current node being visited.
51 current_path (list[Node]): Nodes visited so far on this path (not including *node*).
52 chains (list[Chain]): Accumulator for discovered chains.
53 excluded (set[str]): Mutation types to skip.
54 """
55 if node.mutation_type in excluded:
56 return # stop recursion — this node and its subtree are excluded
58 new_path = current_path + [node]
59 chains.append(Chain(nodes=new_path))
61 for neighbor in graph.successors(node):
62 if neighbor not in new_path: # avoid cycles
63 self._dfs(graph, neighbor, new_path, chains, excluded)