Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find the longest path to each node from a starting node #161

Merged
merged 16 commits into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions .travis.yml
@@ -0,0 +1,24 @@
language: python
sudo: false

python:
- 3.6
before_install:
- wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
- bash miniconda.sh -b -p ~/mc
- export PATH=~/mc/bin:$PATH
- conda config --set always_yes yes --set changeps1 no
- conda config --add channels conda-forge
- conda update conda --yes
- export CONDARC=ci/condarc


install:
- export GIT_FULL_HASH=`git rev-parse HEAD`
- conda create --yes -n testenv --file requirements/run.txt --quiet
- source activate testenv
- python setup.py install


script:
- pytest -v tests
108 changes: 108 additions & 0 deletions conda_forge_tick/path_lengths.py
@@ -0,0 +1,108 @@
"""
Functions to find the longest paths between nodes in a graph.

"""
from copy import deepcopy
from collections import defaultdict

import networkx as nx


def cyclic_topological_sort(graph, source):
"""Return a list of nodes in a graph with cycles in topological order.

Performs a topological sort of `graph` starting from the node `source`.
This is not a true topological sort if `graph` contains cycles, but
any nodes that are not part of a cycle are given in correct topological
order.

Parameters
----------
graph : networkx.classes.digraph.DiGraph
A directed graph.
source : str
The name of the source node.

Returns
-------
list
The nodes of `graph` in topological sort order.

"""

order = []
_visit(graph, source, order)
return reversed(order)


def _visit(graph, node, order):
if graph.node[node].get('visited', False):
return
graph.node[node]['visited'] = True
for n in graph.neighbors(node):
_visit(graph, n, order)
order.append(node)


def get_longest_paths(graph, source):
"""Get the length of the longest path to each node from a source node.

Parameters
----------
graph : networkx.classes.digraph.DiGraph
A directed graph.
source : str
The name of the source node.

Returns
-------
dict
A dictionary where keys are the names of the nodes in `graph` and
values are the lengths of the longest path from `source`.

"""

dist = {node: -float('inf') for node in graph}
dist[source] = 0
visited = []
for u in cyclic_topological_sort(graph, source):
visited.append(u)
for v in graph.neighbors(u):
if v in visited:
continue
if dist[v] < dist[u] + 1:
dist[v] = dist[u] + 1

return dist


def get_levels(graph, source):
"""Get the nodes in each level of a topological sort of a graph starting
from a specified source node.

Parameters
----------
graph : networkx.classes.digraph.DiGraph
A directed graph.
source : str
The name of the source node.

Returns
-------
dict
A dictionary where keys are integers and values are the names of the
nodes in `graph` with longest path length equal to the key.

"""

g2 = deepcopy(graph)
desc = nx.algorithms.descendants(graph, source)
for node in graph.node:
if (node not in desc and node != source):
g2.remove_node(node)

dist = get_longest_paths(g2, source)
levels = defaultdict(set)
for k, v in dist.items():
levels[v].add(k)
return levels
18 changes: 18 additions & 0 deletions tests/test_path_lengths.py
@@ -0,0 +1,18 @@
from conda_forge_tick.path_lengths import get_levels, nx


def test_get_levels():
g = nx.DiGraph([('a', 'd'), ('b', 'd'), ('b', 'e'), ('c', 'e'),
('c', 'h'), ('d', 'f'), ('d', 'g'), ('d', 'h'),
('e', 'g')])
levels = {0: {'a'}, 1: {'d'}, 2: {'f', 'g', 'h'}}
assert get_levels(g, 'a') == levels

g.add_edges_from([('a', 'b'), ('e', 'a')])
levels = {0: {'a'}, 1: {'b'}, 2: {'d', 'e'}, 3: {'f', 'g', 'h'}}
assert get_levels(g, 'a') == levels

g.add_edge('d', 'c')
levels = {0: {'a'}, 1: {'b'}, 2: {'d'}, 3: {'c', 'f'},
4: {'e', 'h'}, 5: {'g'}}
assert get_levels(g, 'a') == levels