def reevaluate_forward(self, to_process, arrow): # print("Re-evaluate forward with " + str(to_process) + " " + str(arrow)) for node in to_process: if self.mode == "heuristic": nzero_effect_nodes = self.effect_edges_graph.get(node) # print("Re-evaluate forward. Currently on node: " + str(node)) # print("nzero-effect-nodes: " + str(nzero_effect_nodes)) elif self.mode == "covernoncolliders": g = set() for n in graph_util.adjacent_nodes(self.graph, node): for m in graph_util.adjacent_nodes(self.graph, n): if graph_util.adjacent(self.graph, n, m): continue if graph_util.is_def_collider(self.graph, m, n, node): continue g.update(m) nzero_effect_nodes = list(g) if nzero_effect_nodes is not None: for w in nzero_effect_nodes: if w == node: continue if not graph_util.adjacent(self.graph, node, w): self.clear_arrow(w, node) self.calculate_arrows_forward(w, node)
def initialize_two_step_edges(self, nodes): for node in nodes: g = set() for n in graph_util.adjacent_nodes(self.graph, node): for m in graph_util.adjacent_nodes(self.graph, n): if node == m: continue if graph_util.adjacent(self.graph, node, m): continue if graph_util.is_def_collider(self.graph, m, n, node): continue g.update(m) for x in g: assert (x is not node) if self.knowledge is not None: if self.knowledge.is_forbidden( node, x) or self.knowledge.is_forbidden(x, node): continue # again, what's the point? if not self.valid_set_by_knowledge(node, set()): continue # TODO: Adjacencies if (x, node) in self.removed_edges: continue self.calculate_arrows_forward(x, node)
def insert(self, x, y, T, bump): """ T is a subset of the neighbors of Y that are not adjacent to (connected by a directed or undirected edge) to X, this should connect X -> Y and for t \in T, direct T -> Y if it's not already directed Definition 12 """ if True: self.tot += 1 print( str(self.tot) + ". Doing an actual insertion with " + str(x) + " -> " + str(y) + " with T: " + str(T) + " and bump: " + str(bump)) if graph_util.adjacent(self.graph, x, y): return False # Adds directed edge self.graph.add_edge(x, y) for node in T: graph_util.undir_to_dir(self.graph, node, y) return True
def undirect_unforced_edges_func(self, node, graph): """Removes directed edges that are not forced by an unshielded collider about node""" node_parents = graph_util.get_parents(graph, node) parents_to_undirect = set(node_parents) # Find any unshielded colliders in node_parents, and orient them for (p1, p2) in itertools.combinations(node_parents, 2): if not graph_util.adjacent(graph, p1, p2): # Have an unshielded collider p1 -> node <- p2, which forces orientation self.oriented.update([(p1, node), (p2, node)]) parents_to_undirect.difference_update([p1, p2]) did_unorient = False for parent in parents_to_undirect: if self.knowledge is not None: must_orient = self.knowledge.is_required(parent, node) or \ self.knowledge.is_forbidden(node, parent) else: must_orient = False if not (parent, node) in self.oriented and not must_orient: # Undirect parent -> node graph_util.remove_dir_edge(graph, parent, node) graph_util.add_undir_edge(graph, parent, node) self.visited.add(node) self.visited.add(parent) # print(f"unorienting {parent} -> {node}") did_unorient = True if did_unorient: for adjacent in graph_util.adjacent_nodes(graph, node): self.direct_stack.append(adjacent) self.direct_stack.append(node)
def bes(self): """BES removes edges from the graph generated by FGES, as added edges can now have negative bump in light of the additions to the graph after those edges were added.""" while len(self.sorted_arrows) > 0: if self.checkpoint_frequency > 0 and ( time.time() - self.last_checkpoint) > self.checkpoint_frequency: self.create_checkpoint() self.last_checkpoint = time.time() arrow = self.sorted_arrows.pop(0) x = arrow.a y = arrow.b if (not (arrow.na_y_x == graph_util.get_na_y_x(self.graph, x, y))) or \ (not graph_util.adjacent(self.graph, x, y)) or (graph_util.has_dir_edge(self.graph, y, x)): continue if not self.valid_delete(x, y, arrow.h_or_t, arrow.na_y_x): continue H = arrow.h_or_t bump = arrow.bump self.delete(x, y, H) meek_rules = MeekRules(knowledge=self.knowledge) meek_rules.orient_implied_subset(self.graph, set([x, y])) self.total_score += bump self.clear_arrow(x, y) if self.verbose: print("BES: Removed arrow " + str(x) + " -> " + str(y) + " with bump -" + str(bump)) visited = self.reapply_orientation(x, y, H) to_process = set() for node in visited: neighbors = graph_util.neighbors(self.graph, node) str_neighbors = self.stored_neighbors[node] if str_neighbors != neighbors: to_process.update([node]) to_process.add(x) to_process.add(y) to_process.update(graph_util.get_common_adjacents( self.graph, x, y)) # TODO: Store graph self.reevaluate_backward(to_process)
def r1_helper(self, node_a, node_b, node_c, graph): if ((not graph_util.adjacent(graph, node_a, node_c)) and (graph_util.has_dir_edge(graph, node_a, node_b) or (node_a, node_b) in self.oriented) and graph_util.has_undir_edge(graph, node_b, node_c)): if not graph_util.is_unshielded_non_collider( graph, node_a, node_b, node_c): return if self.is_arrowpoint_allowed(node_b, node_c): # print("R1: " + str(node_b) + " " + str(node_c)) if (node_a, node_c) not in self.oriented and (node_c, node_a) not in self.oriented and \ (node_b, node_c) not in self.oriented and (node_c, node_b) not in self.oriented: self.direct(node_b, node_c, graph)
def fes(self): """The basic workflow of FGES is to first consider add all edges with positive bump, as defined by the SEMBicScore, to a sorted list (sorted by bump). Edges are popped off this list and added to the graph, after which point the Meek rules are utilized to orient edges in the graph that can be oriented. Then, all relevant bumps are recomputed and the list is resorted. This process is repeated until there remain no edges to add with positive bump.""" # print("Running FES.`.") # print("Length of sorted arrows", len(self.sorted_arrows)) # print(self.arrow_dict) while len(self.sorted_arrows) > 0: if self.checkpoint_frequency > 0 and ( time.time() - self.last_checkpoint) > self.checkpoint_frequency: self.create_checkpoint() self.last_checkpoint = time.time() max_bump_arrow = self.sorted_arrows.pop( 0) # Pops the highest bump edge off the sorted list x = max_bump_arrow.a y = max_bump_arrow.b # print("Popped arrow: " + str(x) + " -> " + str(y)) if graph_util.adjacent(self.graph, x, y): continue na_y_x = graph_util.get_na_y_x(self.graph, x, y) # TODO: max degree checks # print(na_y_x) if max_bump_arrow.na_y_x != na_y_x: continue # print("Past crucial step") if not graph_util.get_t_neighbors(self.graph, x, y).issuperset( max_bump_arrow.h_or_t): continue if not self.valid_insert(x, y, max_bump_arrow.h_or_t, na_y_x): # print("Not valid insert") continue T = max_bump_arrow.h_or_t bump = max_bump_arrow.bump # TODO: Insert should return a bool that we check here inserted = self.insert( x, y, T, bump) # Insert highest bump edge into the graph if not inserted: continue self.total_score += bump # print("Edge set before reapplying orientation: " + str(self.graph.edges())) visited_nodes = self.reapply_orientation( x, y, None) # Orient edges appropriately following insertion # print("Edge set after reapplying orientation: " + str(self.graph.edges())) to_process = set({}) # check whether the (undirected) neighbors of each node in # visited_nodes changed compared to stored neighbors for node in visited_nodes: # gets undirected neighbors new_neighbors = graph_util.neighbors(self.graph, node) stored_neighbors = self.stored_neighbors.get(node) if stored_neighbors != new_neighbors: to_process.add(node) # Reevaluate neighbor nodes to_process.add(x) # Reevaluate edges relating to node x to_process.add(y) # Reevaluate edges relating to node y self.reevaluate_forward(to_process, max_bump_arrow) # Do actual reevaluation