Module genice_core.topology

Arrange edges appropriately.

Expand source code
"""
Arrange edges appropriately.
"""

from logging import getLogger, DEBUG
import networkx as nx
import numpy as np
from typing import Union


def _trace_path(g: nx.Graph, path: list) -> list:
    """trace the path

    Args:
        g (nx.Graph): a linear or a simple cyclic graph.
        path (list): A given path tho be extended

    Returns:
        list: the extended path or cycle
    """
    while True:
        # look at the head of the path
        last, head = path[-2:]
        for next in g.neighbors(head):
            if next != last:
                # go ahead
                break
        else:
            # no next node
            return path
        path.append(next)
        if next == path[0]:
            # is cyclic
            return path


def _find_path(g: nx.Graph) -> list:
    """Find the path in g

    Args:
        g (nx.Graph): a linear or a simple cyclic graph.

    Returns:
        list: the path or cycle
    """
    nodes = list(g.nodes())
    # choose one node
    head = nodes[0]
    # look neighbors
    nei = list(g[head])
    if len(nei) == 0:
        # isolated node
        return []
    elif len(nei) == 1:
        # head is an end node, fortunately.
        return _trace_path(g, [head, nei[0]])
    # look forward
    c0 = _trace_path(g, [head, nei[0]])

    if c0[-1] == head:
        # cyclic graph
        return c0

    # look backward
    c1 = _trace_path(g, [head, nei[1]])
    return c0[::-1] + c1[1:]


def _divide(divg: nx.Graph, nei: list, vertex: int, offset: int):
    # fill by Nones if number of neighbors is less than 4
    nei = (nei + [None, None, None, None])[:4]

    # two neighbor nodes that are passed away to the new node
    migrants = set(np.random.choice(nei, 2, replace=False)) - set([None])

    # new node label
    newVertex = vertex + offset

    # assemble edges
    for migrant in migrants:
        divg.remove_edge(migrant, vertex)
        divg.add_edge(newVertex, migrant)


def _divide_node(divg: nx.Graph, vertex: int, offset: int, numFixedEdges: int):
    nei = list(divg.neighbors(vertex))

    if numFixedEdges == 0:
        _divide(divg, nei, vertex, offset)
    # division is not necessary in any other cases.


def noodlize(g: nx.Graph, fixed: Union[nx.DiGraph, None] = nx.DiGraph()) -> nx.Graph:
    """Divide each vertex of the graph and make a set of paths.

    A new algorithm suggested by Prof. Sakuma, Yamagata University.

    Args:
        g (nx.Graph): An ice-like undirected graph. All vertices must not be >4-degree.
        fixed (Union[nx.DiGraph,None], optional): Specifies the edges whose direction is fixed.. Defaults to None.

    Returns:
        nx.Graph: A graph mode of chains and cycles.
    """

    logger = getLogger()

    fixg = nx.Graph(fixed)  # undirected copy

    offset = len(g)

    # divided graph
    divg = nx.Graph(g)
    for edge in fixed.edges():
        divg.remove_edge(*edge)

    for v in g:
        if fixg.has_node(v):
            nfixed = fixg.degree[v]
        else:
            nfixed = 0
        _divide_node(divg, v, offset, nfixed)

    # divg is made of chains and cycles.
    # divg does not contain the edges in fixed.
    return divg


def _decompose_complex_path(path: list):
    """A generator that divides a complex path with self-crossings to set of simple cycles and paths.

    Args:
        path (list): A complex path

    Yields:
        list: a short and simple path/cycle
    """
    logger = getLogger()
    if len(path) == 0:
        return
    logger.debug(f"decomposing {path}...")
    order = dict()
    order[path[0]] = 0
    store = [path[0]]
    headp = 1
    while headp < len(path):
        node = path[headp]

        if node in order:
            # it is a cycle!
            size = len(order) - order[node]
            cycle = store[-size:] + [node]
            yield cycle

            # remove them from the order[]
            for v in cycle[1:]:
                del order[v]

            # truncate the store
            store = store[:-size]

        order[node] = len(order)
        store.append(node)
        headp += 1
        logger.debug([order, store])
    if len(store) > 1:
        yield store
    logger.debug(f"Done decomposition.")


def split_into_simple_paths(
    nnode: int,
    divg: nx.Graph,
):
    """Set the orientations to the components.

    Args:
        nnode (int): number of nodes in the original graph.
        divg (nx.Graph): the divided graph.

    Yields:
        list: a short and simple path/cycle
    """

    for c in nx.connected_components(divg):
        # a component of c is either a chain or a cycle.
        subg = divg.subgraph(c)
        nn = len(subg)
        ne = len([e for e in subg.edges()])
        assert nn == ne or nn == ne + 1

        # Find a simple path in the doubled graph
        # It must be a simple path or a simple cycle.
        path = _find_path(subg)

        # Flatten then path. It may make the path self-crossing.
        path = [v % nnode for v in path]

        # Divide a long path into simple paths and cycles.
        yield from _decompose_complex_path(path)


def _remove_dummy_nodes(g: Union[nx.Graph, nx.DiGraph]):
    for i in range(-1, -5, -1):
        if g.has_node(i):
            g.remove_node(i)


def balance(fixed: nx.DiGraph, g: nx.Graph, hook=None):
    """Extend the prefixed digraph to make the remaining graph balanced.

    Args:
        fixed (nx.DiGraph): fixed edges
        g (nx.Graph): skeletal graph
    """
    # prepare the perimeter
    perimeter = [
        node
        for node in fixed
        if fixed.in_degree[node] + fixed.out_degree[node] < g.degree[node]
    ]

    while len(perimeter) > 0:
        node = perimeter.pop(0)
        if node < 0:
            continue
        if hook is not None:
            hook(fixed)

        # fill if degree is less than 4
        neighborNodes = list(g[node]) + [-1, -2, -3, -4]
        neighborNodes = neighborNodes[:4]

        while fixed.in_degree(node) > fixed.out_degree(node):
            next = np.random.choice(neighborNodes)
            if not (fixed.has_edge(node, next) or fixed.has_edge(next, node)):
                fixed.add_edge(node, next)
                perimeter.append(next)

        while fixed.in_degree(node) < fixed.out_degree(node):
            next = np.random.choice(neighborNodes)
            if not (fixed.has_edge(node, next) or fixed.has_edge(next, node)):
                fixed.add_edge(next, node)
                perimeter.append(next)

    _remove_dummy_nodes(fixed)

Functions

def balance(fixed: networkx.classes.digraph.DiGraph, g: networkx.classes.graph.Graph, hook=None)

Extend the prefixed digraph to make the remaining graph balanced.

Args

fixed : nx.DiGraph
fixed edges
g : nx.Graph
skeletal graph
Expand source code
def balance(fixed: nx.DiGraph, g: nx.Graph, hook=None):
    """Extend the prefixed digraph to make the remaining graph balanced.

    Args:
        fixed (nx.DiGraph): fixed edges
        g (nx.Graph): skeletal graph
    """
    # prepare the perimeter
    perimeter = [
        node
        for node in fixed
        if fixed.in_degree[node] + fixed.out_degree[node] < g.degree[node]
    ]

    while len(perimeter) > 0:
        node = perimeter.pop(0)
        if node < 0:
            continue
        if hook is not None:
            hook(fixed)

        # fill if degree is less than 4
        neighborNodes = list(g[node]) + [-1, -2, -3, -4]
        neighborNodes = neighborNodes[:4]

        while fixed.in_degree(node) > fixed.out_degree(node):
            next = np.random.choice(neighborNodes)
            if not (fixed.has_edge(node, next) or fixed.has_edge(next, node)):
                fixed.add_edge(node, next)
                perimeter.append(next)

        while fixed.in_degree(node) < fixed.out_degree(node):
            next = np.random.choice(neighborNodes)
            if not (fixed.has_edge(node, next) or fixed.has_edge(next, node)):
                fixed.add_edge(next, node)
                perimeter.append(next)

    _remove_dummy_nodes(fixed)
def noodlize(g: networkx.classes.graph.Graph, fixed: Optional[networkx.classes.digraph.DiGraph] = <networkx.classes.digraph.DiGraph object>) ‑> networkx.classes.graph.Graph

Divide each vertex of the graph and make a set of paths.

A new algorithm suggested by Prof. Sakuma, Yamagata University.

Args

g : nx.Graph
An ice-like undirected graph. All vertices must not be >4-degree.
fixed : Union[nx.DiGraph,None], optional
Specifies the edges whose direction is fixed.. Defaults to None.

Returns

nx.Graph
A graph mode of chains and cycles.
Expand source code
def noodlize(g: nx.Graph, fixed: Union[nx.DiGraph, None] = nx.DiGraph()) -> nx.Graph:
    """Divide each vertex of the graph and make a set of paths.

    A new algorithm suggested by Prof. Sakuma, Yamagata University.

    Args:
        g (nx.Graph): An ice-like undirected graph. All vertices must not be >4-degree.
        fixed (Union[nx.DiGraph,None], optional): Specifies the edges whose direction is fixed.. Defaults to None.

    Returns:
        nx.Graph: A graph mode of chains and cycles.
    """

    logger = getLogger()

    fixg = nx.Graph(fixed)  # undirected copy

    offset = len(g)

    # divided graph
    divg = nx.Graph(g)
    for edge in fixed.edges():
        divg.remove_edge(*edge)

    for v in g:
        if fixg.has_node(v):
            nfixed = fixg.degree[v]
        else:
            nfixed = 0
        _divide_node(divg, v, offset, nfixed)

    # divg is made of chains and cycles.
    # divg does not contain the edges in fixed.
    return divg
def split_into_simple_paths(nnode: int, divg: networkx.classes.graph.Graph)

Set the orientations to the components.

Args

nnode : int
number of nodes in the original graph.
divg : nx.Graph
the divided graph.

Yields

list
a short and simple path/cycle
Expand source code
def split_into_simple_paths(
    nnode: int,
    divg: nx.Graph,
):
    """Set the orientations to the components.

    Args:
        nnode (int): number of nodes in the original graph.
        divg (nx.Graph): the divided graph.

    Yields:
        list: a short and simple path/cycle
    """

    for c in nx.connected_components(divg):
        # a component of c is either a chain or a cycle.
        subg = divg.subgraph(c)
        nn = len(subg)
        ne = len([e for e in subg.edges()])
        assert nn == ne or nn == ne + 1

        # Find a simple path in the doubled graph
        # It must be a simple path or a simple cycle.
        path = _find_path(subg)

        # Flatten then path. It may make the path self-crossing.
        path = [v % nnode for v in path]

        # Divide a long path into simple paths and cycles.
        yield from _decompose_complex_path(path)