def __init__(self, vertexes: List[_Vertex]): """ Manages a partition of nodes which are kept in separate classes according to their rank. Each vertex should already have a non-`None` `vertex.rank` field. The partition created by this constructor respects the constraints (if any) imposed with the attribute `initial_partition_block_id` in each vertex. This object is iterable (returns the classes of vertexes ordered by rank) and may be accessed with the operator `[i]` (returns the `i`-th class in the list). Also, the length of this object is defined as the number of classes of vertexes. :param vertexes: The list of vertexes in the partition. """ self._nvertexes = len(vertexes) max_rank = max(vertex.rank for vertex in vertexes) list_positions = RankedPartition.rank_to_partition_idx(max_rank) + 1 # a list of dicts, one dict for each rank index. in each dict there # are different blocks for different blocks of the labeling set rank_label = [{} for _ in range(list_positions)] for vertex in vertexes: rank_idx = RankedPartition.rank_to_partition_idx(vertex.rank) if vertex.initial_partition_block_id not in rank_label[rank_idx]: # we want to reuse the same xblock for all the vertexes # having the same rank if len(rank_label[rank_idx]) == 0: xblock = _XBlock() else: xblock = list(rank_label[rank_idx].values())[0].xblock rank_label[rank_idx][ vertex.initial_partition_block_id] = _Block([], xblock) rank_label[rank_idx][ vertex.initial_partition_block_id].append_vertex(vertex) # we may not have leafs whith rank -inf, we create a shallow block to # fix the issue if len(rank_label[0]) == 0: rank_label[0][-1] = _Block([], _XBlock()) # at this point there's no need to keep the blocks in a dictionary, # therefore we flatten the innermost dimension self._partition = [ list(rank_idx_dict.values()) for rank_idx_dict in rank_label ]
def test_choose_qblock(): compoundblock = _XBlock() qblocks = [ _QBlock([_Vertex(i) for i in [0, 3, 5]], compoundblock), _QBlock([_Vertex(i) for i in [2, 4]], compoundblock), _QBlock([_Vertex(i) for i in [1, 6, 8]], compoundblock), ] splitter = extract_splitter(compoundblock) assert splitter == qblocks[1] # check if compound block has been modified properly assert compoundblock.qblocks.size == 2 compoundblock_qblocks = set() for i in range(2): compoundblock_qblocks.add(compoundblock.qblocks.nodeat(i).value) assert compoundblock_qblocks == set([qblocks[0], qblocks[2]])
def ranked_split(current_partition: List[_QBlock], B_qblock: _QBlock, max_rank: int) -> List[Tuple[_Vertex]]: """Split the given partition using the block `B_qblock` as *splitter*, then use Ranked *Paige-Tarjan*'s algorithm on the resulting partition. :param current_partition: The current partition as a list of :class:`bispy.utilities.graph_entities._QBlock`. :param B_qblock: The block to be used as *splitter*. :param max_rank: The maximum rank which may be found in the graph. :returns: The output of Ranked *Paige-Tarjan*'s algorithm as a list of tuples of vertexes. """ # initialize x_partition and q_partition x_partition = [ # this also overwrites any information about parent xblocks for qblock _XBlock().append_qblock(qblock) for qblock in current_partition ] q_partition = current_partition # perform Split(B,Q) B_counterimage = build_block_counterimage(B_qblock) new_qblocks, new_compound_xblocks, chaned_qblocks = split(B_counterimage) # reset aux_count for vx in B_counterimage: vx.aux_count = None q_partition.extend(new_qblocks) if max_rank == float("-inf"): max_rank = -1 # note that only new compound xblock are compound xblocks compound_xblocks = RankedCompoundXBlocksContainer(new_compound_xblocks, max_rank) return pta(x_partition, q_partition, compound_xblocks)
def as_bispy_graph( graph: nx.Graph, initial_partition: List[Tuple[int]], build_image, set_count, set_xblock, ) -> Tuple[List[_Vertex], List[_QBlock]]: """ Create the *BisPy* representation of the given graph. There are several options to enable/disable depending on which algorithm in *BisPy* you plan to use. :param graph: The graph. :param initial_partition: The initial partition (or labeling set) imposed on vertexes of the graph. Used to divide nodes in blocks. :param build_image: If `True`, we compute the image of each vertex. :param set_count: If `True`, we set the attribute `count` of each vertex to an appropriate instance of :class:`bispy.utilities.graph_entities._Count`. :param set_xblock: If `True` we set the attribute `xblock` of each block of the partition to an instance of :class:`bispy.utilities.graph_entities._XBlock` (the same for each block). If `False`, we set the attribute to `None`. :returns: A tuple whose items are: 0. List of vertexes in the graph; 1. List of blocks in the initial partition. Both the items are in *BisPy* representation. """ if initial_partition is None: initial_partition = _trivial_initial_partition(len(graph.nodes)) # instantiate QBlocks and Vertexes, put Vertexes into QBlocks and set their # initial block id vertexes = [None for _ in graph.nodes] qblocks = [] initial_x_block = _XBlock() if set_xblock else None for idx, block in enumerate(initial_partition): qblock = _QBlock([], initial_x_block) qblocks.append(qblock) for vx in block: new_vertex = _Vertex(label=vx) vertexes[vx] = new_vertex qblock.append_vertex(new_vertex) new_vertex.initial_partition_block_id = idx if set_count: # holds the references to Count objects to assign to the edges. # count(x) = count(x,V) = |V \cap E({x})| = |E({x})| vertex_count = [None for _ in graph.nodes] else: vertex_count = None # build the counterimage. the image will be constructed using the order # imposed by the rank algorithm for edge in graph.edges: # create an instance of my class Edge my_edge = _Edge(vertexes[edge[0]], vertexes[edge[1]]) if set_count: # if this is the first outgoing edge for the vertex edge[0], we # need to create a new Count instance if not vertex_count[edge[0]]: # in this case None represents the intitial XBlock, namely the # whole V vertex_count[edge[0]] = _Count(my_edge.source) my_edge.count = vertex_count[edge[0]] my_edge.count.value += 1 if build_image: my_edge.source.add_to_image(my_edge) my_edge.destination.add_to_counterimage(my_edge) return (vertexes, qblocks)
def merge_split_phase(qpartition: List[_Block], finishing_time_list: List[_Vertex]) -> List[_Block]: """ The function `MergeAndSplitPhase` from the paper. :param qpartition: The current partition. :param finishing_time_list: List of vertexes in the graph ordered by finishing time. :returns: The updated partition. """ max_rank = float("-inf") for block in qpartition: max_rank = max(max_rank, block.rank) # a dict of lists of blocks (the key is the initial partition ID) # where each couple can't be merged cant_merge_dict = {} # keep track in order to remove the 'visited' flag visited_vertexes = [] # a partition containing all the touched blocks X = [] # visit G in order of decreasing finishing times of the first DFS for vertex in finishing_time_list: # a vertex may be reached more than one time if not vertex.visited: merge_step(vertex, X, visited_vertexes, cant_merge_dict) X = list(filter(lambda block: not block.deteached, X)) # clear visited flag for vx in visited_vertexes: vx.visited = False # reset block.visited flag (was set by first DFS) and tried_merge for block in qpartition: block.visited = False block.tried_merge = False # ------------ # Split phase # ------------ # we need to scale in order to use PTA (and then scale back) scaled_to_nonscaled = [] xblock = _XBlock() for block in X: # this is needed for PTA xblock.append_qblock(block) # set visited flag in order to compute the set (qpartition - X) easily block.visited = True for vx in block.vertexes: # mark as reachable by PTA vx.allow_visit = True # scale label in order to use PTA vx.scale_label(len(scaled_to_nonscaled)) scaled_to_nonscaled.append(vx.label) # build the new qpartition, without the blocks in X (which may be split). # this is just the set qpartition - X new_qpartition = [] for block in qpartition: if not (block.visited or block.deteached): new_qpartition.append(block) else: # now we can clean the flag, this block was already discarded block.visited = False for block in X: for vx in block.vertexes: vx.restrict_to_subgraph(validation=lambda v: v.allow_visit) # apply PTA and append the blocks to the new partition preprocess_initial_partition(X) X2 = paige_tarjan_qblocks(X) new_qpartition.extend(X2) for block in X2: for vx in block.vertexes: # restore the original image/counterimage vx.back_to_original_graph() # clean allow_visit vx.allow_visit = False # restore original label vx.back_to_original_label() # select the blocks which are the result of a split # split, and clean block.visited for block in filter(attrgetter("is_new_qblock"), X2): # split new_qpartition = ranked_split(new_qpartition, block, max_rank) # clean block.is_new_qblock = False return new_qpartition