Module sverchok.utils.topo

Topological sorting.

Performs stable "topological" sort of directed graphs (including graphs with cycles). Possible optimization: instead of counting sort just put vertices on rindex[v] positions if there were no SCCs detected.

Ported from original implementation in Java by Marat Radchenko.

original: https://github.com/slonopotamus/stable-topo-sort/blob/master/test/StableTopoSort.java#L16 algorithm description (russian): https://habr.com/ru/post/451208/

Expand source code
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

"""
Topological sorting.

Performs stable "topological" sort of directed graphs (including graphs with cycles).
Possible optimization: instead of counting sort just put vertices on rindex[v]
positions if there were no SCCs detected.

Ported from original implementation in Java by Marat Radchenko.

original: https://github.com/slonopotamus/stable-topo-sort/blob/master/test/StableTopoSort.java#L16
algorithm description (russian): https://habr.com/ru/post/451208/
"""

from collections import defaultdict

class DoubleStack(object):
    def __init__(self, capacity):
        self.fp = 0 # front pointer
        self.bp = capacity # back pointer
        self.items = [0 for i in range(capacity)]

    def is_empty_front(self):
        return self.fp == 0

    def top_front(self):
        return self.items[self.fp - 1]

    def pop_front(self):
        self.fp -= 1
        return self.items[self.fp]

    def push_front(self, item):
        self.items[self.fp] = item
        self.fp += 1

    def is_empty_back(self):
        return self.bp == len(self.items)

    def top_back(self):
        return self.items[self.bp]

    def pop_back(self):
        value = self.items[self.bp]
        self.bp += 1
        return value
    
    def push_back(self, item):
        self.bp -= 1
        self.items[self.bp] = item

class Node(object):
    def __init__(self, value):
        self.value = value
        self.edges = []
        self.unique_edges = set()
        self.index = 0

    def add_edge_to(self, node):
        self.unique_edges.add(node)
        self.edges.append(node)

    def __str__(self):
        return str(self.value)

class PeaSCC(object):
    def __init__(self, g):
        self.graph = g
        self.rindex = [0 for i in range(len(g))]
        self.index = 1
        self.c = len(g) - 1

        self.vS = DoubleStack(len(g))
        self.iS = DoubleStack(len(g))
        self.root = [False for i in range(len(g))]

    def visit(self, v=None):
        if v is None:
            # Attn! We're walking nodes in reverse
            for i in range(len(self.graph)-1, -1, -1):
                if self.rindex[i] == 0:
                    self.visit(i)
        else:
            self.begin_visiting(v)
            while not self.vS.is_empty_front():
                self.visit_loop()

    def visit_loop(self):
        v = self.vS.top_front()
        i = self.iS.top_front()

        num_edges = len(self.graph[v].edges)

        # Continue traversing out-edges until none left.
        while i <= num_edges:
            # Continuation
            if i > 0:
                # Update status for previously traversed out-edge
                self.finish_edge(v, i - 1)
            if i < num_edges and self.begin_edge(v, i):
                return
            i += 1

        # Finished traversing out edges, update component info
        self.finish_visiting(v)

    def begin_visiting(self, v):
        self.vS.push_front(v)
        self.iS.push_front(0)
        self.root[v] = True
        self.rindex[v] = self.index
        self.index += 1

    def finish_visiting(self, v):
        # Take this vertex off the call stack
        self.vS.pop_front()
        self.iS.pop_front()
        # Update component information
        if self.root[v]:
            self.index -= 1
            while not self.vS.is_empty_back() and self.rindex[v] <= self.rindex[self.vS.top_back()]:
                w = self.vS.pop_back()
                self.rindex[w] = self.c
                self.index -= 1;

            self.rindex[v] = self.c
            self.c -= 1
        else:
            self.vS.push_back(v)

    def begin_edge(self, v, k):
        w = self.graph[v].edges[k].index

        if self.rindex[w] == 0:
            self.iS.pop_front()
            self.iS.push_front(k+1)
            self.begin_visiting(w)
            return True
        else:
            return False

    def finish_edge(self, v, k):
        w = self.graph[v].edges[k].index

        if self.rindex[w] < self.rindex[v]:
            self.rindex[v] = self.rindex[w]
            self.root[v] = False

class StableTopoSort(object):
    @staticmethod
    def reverse_counting_sort(nodes, rindex):
        count = [0 for i in range(len(nodes))]

        for i in range(len(rindex)):
            cindex = len(nodes) - 1 - rindex[i]
            count[cindex] += 1

        for i in range(1, len(count)):
            count[i] += count[i - 1]

        output = [None for i in range(len(nodes))]
        for i in range(len(output)):
            cindex = len(nodes) - 1 - rindex[i]

            # Attn! We're sorting in reverse
            output_index = len(output) - count[cindex]

            output[output_index] = nodes[i]
            count[cindex] -= 1

        return output

    @staticmethod
    def stable_topo_sort(nodes):
        # 0. Remember where each node was
        for i in range(len(nodes)):
            nodes[i].index = i

        # 1. Sort edges according to node indices
        for i in range(len(nodes)):
            nodes[i].edges.sort(key = lambda o: o.index)

        # 2. Perform Tarjan SCC
        scc = PeaSCC(nodes)
        scc.visit()

        # 3. Perform *reverse* counting sort
        return StableTopoSort.reverse_counting_sort(nodes, scc.rindex)

def sort_by_incidence(vertices, edges):
    incidence = defaultdict(lambda: 0)
    for i, j in edges:
        incidence[i] += 1
        incidence[j] += 1
    
    indicies = list(range(len(vertices)))
    indicies.sort(key = lambda i: incidence[i])

    reverse_index = dict()
    vertices_out = []
    for new_index, old_index in enumerate(indicies):
        reverse_index[old_index] = new_index
        vertices_out.append(vertices[old_index])

    edges_out = []
    for i, j in edges:
        edges_out.append((reverse_index[i], reverse_index[j]))

    return vertices_out, edges_out

def stable_topo_sort(vertices, edges):
    graph = []
    nodes = dict()

    #vertices, edges = sort_by_incidence(vertices, edges)

    for i, vertex in enumerate(vertices):
        node = Node(vertex)
        graph.append(node)
        nodes[i] = node

    for i, j in edges:
        from_node = nodes[i]
        to_node = nodes[j]
        from_node.add_edge_to(to_node)

    graph = StableTopoSort.stable_topo_sort(graph)
    return [node.value for node in graph]

Functions

def sort_by_incidence(vertices, edges)
Expand source code
def sort_by_incidence(vertices, edges):
    incidence = defaultdict(lambda: 0)
    for i, j in edges:
        incidence[i] += 1
        incidence[j] += 1
    
    indicies = list(range(len(vertices)))
    indicies.sort(key = lambda i: incidence[i])

    reverse_index = dict()
    vertices_out = []
    for new_index, old_index in enumerate(indicies):
        reverse_index[old_index] = new_index
        vertices_out.append(vertices[old_index])

    edges_out = []
    for i, j in edges:
        edges_out.append((reverse_index[i], reverse_index[j]))

    return vertices_out, edges_out
def stable_topo_sort(vertices, edges)
Expand source code
def stable_topo_sort(vertices, edges):
    graph = []
    nodes = dict()

    #vertices, edges = sort_by_incidence(vertices, edges)

    for i, vertex in enumerate(vertices):
        node = Node(vertex)
        graph.append(node)
        nodes[i] = node

    for i, j in edges:
        from_node = nodes[i]
        to_node = nodes[j]
        from_node.add_edge_to(to_node)

    graph = StableTopoSort.stable_topo_sort(graph)
    return [node.value for node in graph]

Classes

class DoubleStack (capacity)
Expand source code
class DoubleStack(object):
    def __init__(self, capacity):
        self.fp = 0 # front pointer
        self.bp = capacity # back pointer
        self.items = [0 for i in range(capacity)]

    def is_empty_front(self):
        return self.fp == 0

    def top_front(self):
        return self.items[self.fp - 1]

    def pop_front(self):
        self.fp -= 1
        return self.items[self.fp]

    def push_front(self, item):
        self.items[self.fp] = item
        self.fp += 1

    def is_empty_back(self):
        return self.bp == len(self.items)

    def top_back(self):
        return self.items[self.bp]

    def pop_back(self):
        value = self.items[self.bp]
        self.bp += 1
        return value
    
    def push_back(self, item):
        self.bp -= 1
        self.items[self.bp] = item

Methods

def is_empty_back(self)
Expand source code
def is_empty_back(self):
    return self.bp == len(self.items)
def is_empty_front(self)
Expand source code
def is_empty_front(self):
    return self.fp == 0
def pop_back(self)
Expand source code
def pop_back(self):
    value = self.items[self.bp]
    self.bp += 1
    return value
def pop_front(self)
Expand source code
def pop_front(self):
    self.fp -= 1
    return self.items[self.fp]
def push_back(self, item)
Expand source code
def push_back(self, item):
    self.bp -= 1
    self.items[self.bp] = item
def push_front(self, item)
Expand source code
def push_front(self, item):
    self.items[self.fp] = item
    self.fp += 1
def top_back(self)
Expand source code
def top_back(self):
    return self.items[self.bp]
def top_front(self)
Expand source code
def top_front(self):
    return self.items[self.fp - 1]
class Node (value)
Expand source code
class Node(object):
    def __init__(self, value):
        self.value = value
        self.edges = []
        self.unique_edges = set()
        self.index = 0

    def add_edge_to(self, node):
        self.unique_edges.add(node)
        self.edges.append(node)

    def __str__(self):
        return str(self.value)

Methods

def add_edge_to(self, node)
Expand source code
def add_edge_to(self, node):
    self.unique_edges.add(node)
    self.edges.append(node)
class PeaSCC (g)
Expand source code
class PeaSCC(object):
    def __init__(self, g):
        self.graph = g
        self.rindex = [0 for i in range(len(g))]
        self.index = 1
        self.c = len(g) - 1

        self.vS = DoubleStack(len(g))
        self.iS = DoubleStack(len(g))
        self.root = [False for i in range(len(g))]

    def visit(self, v=None):
        if v is None:
            # Attn! We're walking nodes in reverse
            for i in range(len(self.graph)-1, -1, -1):
                if self.rindex[i] == 0:
                    self.visit(i)
        else:
            self.begin_visiting(v)
            while not self.vS.is_empty_front():
                self.visit_loop()

    def visit_loop(self):
        v = self.vS.top_front()
        i = self.iS.top_front()

        num_edges = len(self.graph[v].edges)

        # Continue traversing out-edges until none left.
        while i <= num_edges:
            # Continuation
            if i > 0:
                # Update status for previously traversed out-edge
                self.finish_edge(v, i - 1)
            if i < num_edges and self.begin_edge(v, i):
                return
            i += 1

        # Finished traversing out edges, update component info
        self.finish_visiting(v)

    def begin_visiting(self, v):
        self.vS.push_front(v)
        self.iS.push_front(0)
        self.root[v] = True
        self.rindex[v] = self.index
        self.index += 1

    def finish_visiting(self, v):
        # Take this vertex off the call stack
        self.vS.pop_front()
        self.iS.pop_front()
        # Update component information
        if self.root[v]:
            self.index -= 1
            while not self.vS.is_empty_back() and self.rindex[v] <= self.rindex[self.vS.top_back()]:
                w = self.vS.pop_back()
                self.rindex[w] = self.c
                self.index -= 1;

            self.rindex[v] = self.c
            self.c -= 1
        else:
            self.vS.push_back(v)

    def begin_edge(self, v, k):
        w = self.graph[v].edges[k].index

        if self.rindex[w] == 0:
            self.iS.pop_front()
            self.iS.push_front(k+1)
            self.begin_visiting(w)
            return True
        else:
            return False

    def finish_edge(self, v, k):
        w = self.graph[v].edges[k].index

        if self.rindex[w] < self.rindex[v]:
            self.rindex[v] = self.rindex[w]
            self.root[v] = False

Methods

def begin_edge(self, v, k)
Expand source code
def begin_edge(self, v, k):
    w = self.graph[v].edges[k].index

    if self.rindex[w] == 0:
        self.iS.pop_front()
        self.iS.push_front(k+1)
        self.begin_visiting(w)
        return True
    else:
        return False
def begin_visiting(self, v)
Expand source code
def begin_visiting(self, v):
    self.vS.push_front(v)
    self.iS.push_front(0)
    self.root[v] = True
    self.rindex[v] = self.index
    self.index += 1
def finish_edge(self, v, k)
Expand source code
def finish_edge(self, v, k):
    w = self.graph[v].edges[k].index

    if self.rindex[w] < self.rindex[v]:
        self.rindex[v] = self.rindex[w]
        self.root[v] = False
def finish_visiting(self, v)
Expand source code
def finish_visiting(self, v):
    # Take this vertex off the call stack
    self.vS.pop_front()
    self.iS.pop_front()
    # Update component information
    if self.root[v]:
        self.index -= 1
        while not self.vS.is_empty_back() and self.rindex[v] <= self.rindex[self.vS.top_back()]:
            w = self.vS.pop_back()
            self.rindex[w] = self.c
            self.index -= 1;

        self.rindex[v] = self.c
        self.c -= 1
    else:
        self.vS.push_back(v)
def visit(self, v=None)
Expand source code
def visit(self, v=None):
    if v is None:
        # Attn! We're walking nodes in reverse
        for i in range(len(self.graph)-1, -1, -1):
            if self.rindex[i] == 0:
                self.visit(i)
    else:
        self.begin_visiting(v)
        while not self.vS.is_empty_front():
            self.visit_loop()
def visit_loop(self)
Expand source code
def visit_loop(self):
    v = self.vS.top_front()
    i = self.iS.top_front()

    num_edges = len(self.graph[v].edges)

    # Continue traversing out-edges until none left.
    while i <= num_edges:
        # Continuation
        if i > 0:
            # Update status for previously traversed out-edge
            self.finish_edge(v, i - 1)
        if i < num_edges and self.begin_edge(v, i):
            return
        i += 1

    # Finished traversing out edges, update component info
    self.finish_visiting(v)
class StableTopoSort
Expand source code
class StableTopoSort(object):
    @staticmethod
    def reverse_counting_sort(nodes, rindex):
        count = [0 for i in range(len(nodes))]

        for i in range(len(rindex)):
            cindex = len(nodes) - 1 - rindex[i]
            count[cindex] += 1

        for i in range(1, len(count)):
            count[i] += count[i - 1]

        output = [None for i in range(len(nodes))]
        for i in range(len(output)):
            cindex = len(nodes) - 1 - rindex[i]

            # Attn! We're sorting in reverse
            output_index = len(output) - count[cindex]

            output[output_index] = nodes[i]
            count[cindex] -= 1

        return output

    @staticmethod
    def stable_topo_sort(nodes):
        # 0. Remember where each node was
        for i in range(len(nodes)):
            nodes[i].index = i

        # 1. Sort edges according to node indices
        for i in range(len(nodes)):
            nodes[i].edges.sort(key = lambda o: o.index)

        # 2. Perform Tarjan SCC
        scc = PeaSCC(nodes)
        scc.visit()

        # 3. Perform *reverse* counting sort
        return StableTopoSort.reverse_counting_sort(nodes, scc.rindex)

Static methods

def reverse_counting_sort(nodes, rindex)
Expand source code
@staticmethod
def reverse_counting_sort(nodes, rindex):
    count = [0 for i in range(len(nodes))]

    for i in range(len(rindex)):
        cindex = len(nodes) - 1 - rindex[i]
        count[cindex] += 1

    for i in range(1, len(count)):
        count[i] += count[i - 1]

    output = [None for i in range(len(nodes))]
    for i in range(len(output)):
        cindex = len(nodes) - 1 - rindex[i]

        # Attn! We're sorting in reverse
        output_index = len(output) - count[cindex]

        output[output_index] = nodes[i]
        count[cindex] -= 1

    return output
def stable_topo_sort(nodes)
Expand source code
@staticmethod
def stable_topo_sort(nodes):
    # 0. Remember where each node was
    for i in range(len(nodes)):
        nodes[i].index = i

    # 1. Sort edges according to node indices
    for i in range(len(nodes)):
        nodes[i].edges.sort(key = lambda o: o.index)

    # 2. Perform Tarjan SCC
    scc = PeaSCC(nodes)
    scc.visit()

    # 3. Perform *reverse* counting sort
    return StableTopoSort.reverse_counting_sort(nodes, scc.rindex)