def iterate(graph, wl_state, iteration, test_mode=False): '''Performs one iteration of the Weisfeiler-Lehman algorithm. :param graph: A Networkx graph or a Hypergraph :param wl_state: A dictionary containing 2 sub-dictionaries: "labels" contains a mapping from the full original graph labels or labels generated during the Weisfeiler & Lehman iterations to their corresponding WL short unique labels; "next_labels" contains the next label number for each WL iteration. :param iteration: The current iteration number. :return A tuple of the form (new_graph, new_labels_list), where new_graph is the resulting graph from the iteration, new_labels_list is the new list of labels for the current iteration of the algorithm. ''' def get_new_label(node, neighbors): def get_direction(u, v): '''Get the direction of the edge between nodes u and v ''' res = 0 if type(graph) is not Hypergraph: if graph.has_edge(u, v) and graph.has_edge(v, u): res = 0 elif graph.has_edge(u, v): res = 1 elif graph.has_edge(v, u): res = -1 else: raise Exception("There is no edge between {0} and {1}.".format(u, v)) else: if u.startswith(u"e_"): # u represents an edge -> all the neighbors are nodes n = v e = u k = -1 else: # u represents a node -> all the neighbors are edges n = u e = v k = 1 dir_perms = graph.node[e]["direction"] dir_perm_0 = next(iter(dir_perms)) if len(dir_perm_0) > 2: raise Exception("Weisfeiler-Lehman is not implemented for hypergraphs with edges of order > 2.") if len(dir_perms) != 1: res = 0 elif dir_perm_0.index(n) == 0: res = k elif dir_perm_0.index(n) == 1: res = -k else: raise Exception("Strange direction encoding of an edge. Are {0} and {1} connected?".format(u, v)) return "out" if res > 0 else "in" if res < 0 else "any" node_label = graph.node[node]["labels"][0] label_extensions = {"any" : [], "in" : [], "out" : []} for neighbor in neighbors: neighbor_label = graph.node[neighbor]["labels"][0] direction = get_direction(node, neighbor) label_extensions[direction].append(neighbor_label) label_extension = [] for direction in ["any", "in", "out"]: if label_extensions[direction]: label_extensions[direction].sort() label_extension.append("{0}({1})".format(direction, ",".join(label_extensions[direction]))) label_extension = ",".join(label_extension) return "{0};{1}".format(node_label, label_extension) new_graph = graph.copy() if iteration not in wl_state["next_labels"]: wl_state["next_labels"][iteration] = 0 if test_mode: nodes = sorted(graph.node, key=lambda n: graph.node[n]["labels"][0]) else: nodes = graph.node for node in nodes: if type(graph) is not Hypergraph: neighbors = nxext.get_all_neighbors(graph, node) else: neighbors = graph.bipartite_graph.neighbors(node) new_node_label = get_new_label(node, neighbors) if new_node_label not in wl_state["labels"]: wl_state["labels"][new_node_label] = "wl_{0}.{1}".format(iteration, wl_state["next_labels"][iteration]) wl_state["next_labels"][iteration] += 1 new_graph.node[node]["labels"] = [wl_state["labels"][new_node_label]] return new_graph, wl_state
def process_raw_feature(raw_feature, hypergraph, max_nodes=6): '''Turns a raw feature to a usable feature or a collection of features, depending on the type of the rule according to which the feature was reduced (fixed, pattern or dynamic). :param raw_feature: A ReducibleFeature extracted from Arnborg & Proskurowski :param hypergraph: A Hypergraph which contains the nodes of the raw feature. :param max_nodes: (default value 6) A number of nodes that a pattern or a dynamic feature can have before being disassembled in subfeatures of size max_nodes. :return A collection containing one or more features depending on the type of the raw feature. ''' assert type(raw_feature) is ReducibleFeature assert type(hypergraph) is Hypergraph assert max_nodes > 3 def get_feature_type(raw_feature): '''Get the type of the raw feature according to the rule it was reduced by. :param raw_feature: A ReducibleFeature. :return Type of the raw feature: 0 for "fixed", 1 for "pattern", 2 for "dynamic". ''' rule = raw_feature.get_full_rule() if rule in ["2.1.0.0", "2.2.0.0", "4.2.0.0", "4.3.0.0", "5.2.3.1"]: return 1 elif rule in ["5.2.2.0", "5.2.3.2", "5.2.4.0"]: return 2 else: return 0 def sliding_window(seq, window_size): # Returns a sliding window (of width window_size) over data from the iterable seq # s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... it = iter(seq) result = tuple(islice(it, window_size)) if len(result) == window_size: yield result for elem in it: result = result[1:] + (elem,) yield result feature_type = get_feature_type(raw_feature) if feature_type == 1: # pattern nodes_count = raw_feature.number_of_nodes() if nodes_count > max_nodes: rule = raw_feature.get_full_rule() if rule == "2.1.0.0": # chain: extract all subpaths of length max_nodes using a sliding window s = raw_feature.peripheral_nodes[0] t = raw_feature.peripheral_nodes[1] path = [s] + raw_feature.reducible_nodes + [t] for subpath in sliding_window(path, max_nodes): yield hypergraph.subgraph_with_labels(set(subpath)) raise StopIteration elif rule == "2.2.0.0": # ring: extract all subpaths of length max_nodes using a sliding window cycle = raw_feature.peripheral_nodes + raw_feature.reducible_nodes for subpath in sliding_window(cycle + cycle[:max_nodes - 1], max_nodes): yield hypergraph.subgraph_with_labels(set(subpath)) raise StopIteration elif rule == "4.2.0.0": # buddy: If there are more than 3 buddies create a buddy # configuration with 3 buddies for each possible combination assert len(raw_feature.peripheral_nodes) == 3 buddy_nodes = raw_feature.reducible_nodes for buddy_nodes_subgroup in itertools.combinations(buddy_nodes, max_nodes - 3): yield hypergraph.subgraph_with_labels(set(buddy_nodes_subgroup) | set(raw_feature.peripheral_nodes)) raise StopIteration elif rule == "4.3.0.0": # cube: similar approach as for wheel (5.2.3.1) if nodes_count > max_nodes + 1: reducible_neigh = [set(hypergraph.neighbors(node)) for node in raw_feature.reducible_nodes] hub_node = reduce(lambda a, b: a.intersection(b), reducible_neigh) ring_subgraph = hypergraph.subgraph(raw_feature.reducible_nodes | (raw_feature.peripheral_nodes - hub_node)) ring = nx.cycle_basis(ring_subgraph) if len(ring) > 0: ring = ring[0] for subpath in sliding_window(ring + ring[:max_nodes - 1], max_nodes): yield hypergraph.subgraph_with_labels(set(subpath) | hub_node) raise StopIteration else: # if there is no ring in the feature, treat it as a fixed feature # TODO: This can lead to a large number of shingles. Better solution? pass elif rule == "5.2.3.1": # wheel: extract all cake-slice subpaths of length max_node using a sliding window if nodes_count > max_nodes + 1: ring_subgraph = hypergraph.subgraph(raw_feature.reducible_nodes) ring = nx.cycle_basis(ring_subgraph) if len(ring) > 0: ring = ring[0] for subpath in sliding_window(ring + ring[:max_nodes - 1], max_nodes): yield hypergraph.subgraph_with_labels(set(subpath) | set(raw_feature.peripheral_nodes)) raise StopIteration else: # if there is no ring in the feature, treat it as a fixed feature # TODO: This can lead to a large number of shingles. Better solution? pass elif feature_type == 2: # dynamic (the reducible nodes are always of degree 3): # for each pair of adjacent nodes u, v let a new feature be # the subgraph that contains u, v and all neighbors of u and v. nodes_count = raw_feature.number_of_nodes() if nodes_count > max_nodes: nodes = set(raw_feature.reducible_nodes) | set(raw_feature.peripheral_nodes) feature_graph = hypergraph.subgraph(nodes) adj_nodes = nxext.get_all_adjacent_nodes(feature_graph) neighbors = {node: nxext.get_all_neighbors(feature_graph, node) for node in nodes} for u, v in adj_nodes: node_subgroup = set([u, v] + neighbors[u] + neighbors[v]) yield hypergraph.subgraph_with_labels(set(node_subgroup)) raise StopIteration # fixed or pattern/dynamic with <= max_nodes number of nodes yield raw_feature.as_subgraph(hypergraph)