def test_well_founded_topological(): g = nx.DiGraph() g.add_nodes_from(range(8)) g.add_edges_from([(0, 1), (1, 2), (3, 4), (4, 5), (6, 7), (7, 6)]) partition = [(0, 3), (1, 4), (2, 5), (6, 7)] vertexes, _ = decorate_nx_graph(g, partition) max_rank = max(map(lambda vx: vx.rank, vertexes)) qpartition = [block for ls in RankedPartition(vertexes) for block in ls] topo = build_well_founded_topological_list( qpartition, vertexes[5], max_rank ) assert len(topo) == 6 last_rank = None for vx in topo: assert vx.wf if last_rank is not None: assert last_rank <= vx.rank last_rank = vx.rank
def test_iter(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) idx = 0 for r in partition: assert r == partition._partition[idx] idx += 1
def test_split_upper_ranks(graph): vertexes, _ = decorate_nx_graph(graph) max_rank = max(vertex.rank for vertex in vertexes) partition_length = 0 if max_rank == float("-inf") else max_rank + 2 for idx in range(partition_length): partition = RankedPartition(vertexes) split_upper_ranks(partition, partition[idx][0]) assert all( check_block_stability(partition[idx][0], upper_rank_block) for upper_idx in range(idx + 1, partition_length) for upper_rank_block in partition[upper_idx])
def split_upper_ranks(partition: RankedPartition, block: _Block): """Split the blocks whose `rank` is **greater** than `block.rank` using `block` as *splitter*. :param partition: The current partition. :param block: The splitter block. """ block_counterimage = build_block_counterimage(block) modified_blocks = [] for vertex in block_counterimage: # if this is an upper-rank node with respect to the collapsed block, we # need to split. the split is not needed if the block is a singoletto. if vertex.rank > block.rank and not ( vertex.qblock.split_helper_block is None and vertex.qblock.size <= 1): # if needed, create the aux block to help during the splitting # phase if vertex.qblock.split_helper_block is None: vertex.qblock.initialize_split_helper_block() modified_blocks.append(vertex.qblock) new_vertex_block = vertex.qblock.split_helper_block # remove the vertex in the counterimage from its current block vertex.qblock.remove_vertex(vertex) # put the vertex in the counterimage in the aux block new_vertex_block.append_vertex(vertex) # insert the new blocks in the partition, and then reset aux block for each # modified block. for mod_block in modified_blocks: # we use the rank of aux block because we're sure it's not None partition.append_at_rank( block=mod_block.split_helper_block, rank=mod_block.split_helper_block.rank, ) mod_block.split_helper_block = None
def test_is_in_image(): graph = nx.DiGraph() graph.add_nodes_from(range(5)) graph.add_edges_from([(0, 1), (1, 2), (2, 3), (4, 1)]) vertexes, _ = decorate_nx_graph(graph) RankedPartition(vertexes) assert is_in_image(vertexes[0].qblock, vertexes[1].qblock) assert not is_in_image(vertexes[1].qblock, vertexes[0].qblock) assert is_in_image(vertexes[1].qblock, vertexes[2].qblock) assert is_in_image(vertexes[2].qblock, vertexes[3].qblock) assert not is_in_image(vertexes[0].qblock, vertexes[4].qblock) assert not is_in_image(vertexes[4].qblock, vertexes[0].qblock)
def test_merge_split_resets_visited_allowvisit_oldqblockid(): g = nx.DiGraph() g.add_nodes_from(range(5)) g.add_edges_from([(0, 1), (1, 0), (2, 1), (2, 4), (3, 1)]) partition = [(2, 3), (1, 0), (4,)] vertexes, _ = decorate_nx_graph(g, partition) qblocks = [block for ls in RankedPartition(vertexes) for block in ls] finishing_time_list = [vertexes[2], vertexes[1], vertexes[0]] merge_split_phase(qblocks, finishing_time_list) assert all([not vertex.visited for vertex in vertexes]) assert all([not vertex.allow_visit for vertex in vertexes])
def test_different_blocks_initial_partition(): graph = nx.balanced_tree(2, 3, create_using=nx.DiGraph) initial_partition = [ (0, 1, 2), (3, 4), (5, 6), (7, 8, 9, 10), (11, 12, 13), (14, ), ] vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3), initial_partition) partition = RankedPartition(vertexes) assert len(partition[1]) == 3 assert len(partition[2]) == 2
def test_create_initial_partition(graph): vertexes, _ = decorate_nx_graph(graph) partition = RankedPartition(vertexes) # at least one block per rank, except for float('-inf') assert all(len(partition[idx]) for idx in range(1, len(partition))) # right vertexes in the right place for idx in range(len(partition)): rank = float("-inf") if idx == 0 else idx - 1 # right number of vertexes assert partition[idx][0].vertexes.size == [ vertex.rank == rank for vertex in vertexes ].count(True) # right rank assert all(vertex.rank == rank for vertex in partition[idx][0].vertexes)
def test_merge_split_resets_visited_triedmerge_qblocks(): g = nx.DiGraph() g.add_nodes_from(range(5)) g.add_edges_from([(0, 1), (1, 0), (2, 1), (2, 4), (3, 1)]) partition = [(2, 3), (1, 0), (4,)] vertexes, _ = decorate_nx_graph(g, partition) qblocks = [block for ls in RankedPartition(vertexes) for block in ls] qblocks[0].visited = True qblocks[2].visited = True finishing_time_list = [vertexes[2], vertexes[1], vertexes[0]] qpartition = merge_split_phase(qblocks, finishing_time_list) assert all([not block.visited for block in qpartition]) assert all([not block.tried_merge for block in qpartition])
def test_add_edge(): graph = nx.DiGraph() graph.add_nodes_from(range(5)) graph.add_edges_from([(0, 1), (1, 2), (2, 3), (4, 1)]) vertexes, _ = decorate_nx_graph(graph) ranked_partition = RankedPartition(vertexes) partition = [] for rank in ranked_partition: for block in ranked_partition: partition.append(block) edge1 = add_edge(vertexes[0], vertexes[3]) assert edge1.count is not None assert edge1.count.value == 2 edge2 = add_edge(vertexes[3], vertexes[4]) assert edge2.count is not None assert edge2.count.value == 1
def dovier_piazza_policriti_partition( partition: RankedPartition, ) -> Tuple[RankedPartition, List[List[_Vertex]]]: """Apply *Dovier-Piazza-Policriti*'s algorithm to the given ranked partition. :param partition: A ranked partition (:math:`P` in the paper). :returns: A tuple such that the first item is the partition at the end of the algorithm (which at this point is made of blocks of size 1 containing only the vertexes which survived the collapse), and the second is a list which maps a survivor nodes to the list of nodes collapsed to that survivor node. """ # maps each survivor node to a list of nodes collapsed into it collapse_map = [None for _ in range(partition.nvertexes)] # loop over the ranks for partition_idx in range(len(partition)): if len(partition[partition_idx]) == 1: if len(partition[partition_idx][0].vertexes): block = partition[partition_idx][0] survivor_vertex, collapsed_vertexes = collapse(block) if survivor_vertex is not None: # update the collapsed nodes map collapse_map[survivor_vertex.label] = collapsed_vertexes # update the partition split_upper_ranks(partition, block) # OPTIMIZATION: if at the current rank we only have blocks of single # vertexes, skip this step. elif any(map(lambda block: block.size > 1, partition[partition_idx])): current_label = 0 for block in partition[partition_idx]: for vertex in block.vertexes: # scale vertex vertex.scale_label(current_label) current_label += 1 # exclude nodes having the wrong rank from the image and # counterimage of the vertex. from now they're gone # forever. vertex.restrict_to_subgraph( validation=lambda vx: vx.rank == vertex.rank) # apply PTA to the subgraph at the current examined rank # CAREFUL: if you debug here, you'll see that there are some # "duplicate" nodes (nodes with the same label in different blocks # of the partition). this happens becaus of the SCALING (which is # used to pass a normal graph to PTA) rscp = paige_tarjan_qblocks(partition[partition_idx]) # clear the partition at the current rank partition.clear_index(partition_idx) # insert the new blocks in the partition at the current rank, and # collapse each block. for block in rscp: block_vertexes = [] for scaled_vertex in block.vertexes: scaled_vertex.back_to_original_label() scaled_vertex.back_to_original_graph() block_vertexes.append(scaled_vertex) # we can set XBlock to None because PTA won't be called again # on these blocks internal_block = _Block(block_vertexes, None) survivor_vertex, collapsed_vertexes = collapse(internal_block) if survivor_vertex is not None: # update the collapsed nodes map collapse_map[survivor_vertex.label] = collapsed_vertexes # add the new block to the partition partition.append_at_index(internal_block, partition_idx) # update the upper ranks with respect to this block split_upper_ranks(partition, internal_block) else: for block in partition[partition_idx]: # update the upper ranks with respect to this block split_upper_ranks(partition, block) return (partition, collapse_map)
def test_get_item(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) assert partition[1] == partition._partition[1]
def test_clear_index(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) partition.clear_index(1) assert len(partition[1]) == 0
def test_scc_leaf_length(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) assert partition[0][0].size == 0
def test_len(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) assert len(partition) == 5
def test_append_at_index(): vertexes, _ = decorate_nx_graph(nx.balanced_tree(2, 3)) partition = RankedPartition(vertexes) block = _QBlock([], None) partition.append_at_index(block, 1) assert partition[1][-1] == block
def test_nvertexes(graph): vertexes, _ = decorate_nx_graph(graph) partition = RankedPartition(vertexes) assert partition.nvertexes == len(graph.nodes)
def dovier_piazza_policriti( graph: nx.Graph, initial_partition: List[Tuple[int]] = None, is_integer_graph: bool = False, ) -> List[Tuple]: """Compute the RSCP/maximum bisimulation of the given graph using *Dovier-Piazza-Policriti*'s algorithm. Example: >>> graph = networkx.balanced_tree(2,3) >>> dovier_piazza_policriti(graph) [(7, 8, 9, 10, 11, 12, 13, 14), (3, 4, 5, 6), (1, 2), (0,)] This function works with integer graph (nodes are integers starting from 0 and form an interval without holes). If the given graph is non-integer it is converted to an isomorphic integer graph automatically (unless `is_integer_graph` is `True`) and then re-converted to its original form after the end of the computation. For this reason nodes of `graph` **must** be hashable objects. .. warning:: Using a non integer graph and setting `is_integer_graph` to `True` will probably make the function fail with an exception, or, even worse, return a wrong output. :param graph: The input graph. :param initial_partition: The initial partition (or labeling set). Defaults to `None`, in which case the trivial labeling set (one block which contains all the nodes) is used. :param is_integer_graph: If `True`, we do not check if the given graph is integer (saves time). If `is_integer_graph` is `True` but the graph is not integer the output may be wrong. Defaults to False. :returns: The RSCP/maximum bisimulation of the given labeling set as a list of tuples, each of which contains bisimilar nodes. """ if not isinstance(graph, nx.DiGraph): raise Exception("graph should be a directed graph (nx.DiGraph)") # if True, the input graph is already an integer graph original_graph_is_integer = is_integer_graph or check_normal_integer_graph( graph) if not original_graph_is_integer: # convert the graph to an "integer" graph integer_graph, node_to_idx = convert_to_integer_graph(graph) else: integer_graph = graph vertexes, _ = decorate_nx_graph(integer_graph, initial_partition) partition = RankedPartition(vertexes) tp = dovier_piazza_policriti_partition(partition) collapsed_partition, collapse_map = tp # from the collapsed partition obtained from FBA, build the RSCP (external # representation, List[Tuple[int]]) rscp = [] for rank in collapsed_partition: for block in rank: if block.vertexes.size > 0: block_survivor_node = block.vertexes.first.value block_vertexes = [block_survivor_node.label] if collapse_map[block_survivor_node.label] is not None: block_vertexes.extend( map( lambda vertex: vertex.label, collapse_map[block_survivor_node.label], )) rscp.append(tuple(block_vertexes)) if original_graph_is_integer: return rscp else: return back_to_original(rscp, node_to_idx)
def test_rank_to_partition_idx(rank, expected): assert RankedPartition.rank_to_partition_idx(rank) == expected