Skip to content

Commit

Permalink
Merge pull request #5 from PatrickOHara/pctsp-37-sparse
Browse files Browse the repository at this point in the history
Remove edges from TSPLIB to make sparse graphs
  • Loading branch information
PatrickOHara authored Nov 29, 2020
2 parents fbbcdb8 + 13a410b commit c87a1bb
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 1 deletion.
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ good-names=i,
v,
w,
G,
H,
T,
x,
y,
Expand Down
27 changes: 27 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from tspwplib.types import Alpha, Generation, InstanceName


# add parser options


def pytest_addoption(parser):
"""Add option to enable travis specific options"""
parser.addoption(
Expand All @@ -24,6 +27,9 @@ def pytest_addoption(parser):
)


# fixtures for filepaths


@pytest.fixture(scope="function")
def tsplib_root(request) -> Path:
"""Root of tsplib95 data"""
Expand All @@ -36,6 +42,9 @@ def oplib_root(request) -> Path:
return Path(request.config.getoption("--oplib-root"))


# fixtures for types


@pytest.fixture(
scope="function",
params=[
Expand Down Expand Up @@ -67,3 +76,21 @@ def alpha(request) -> Alpha:
def instance_name(request) -> InstanceName:
"""Loop through valid instance names"""
return request.param


# fixtures for complete graphs


@pytest.fixture(
scope="function",
params=[
0.0,
0.1,
0.5,
0.9,
1.0,
],
)
def edge_removal_probability(request) -> float:
"""Different valid values for probability of removing an edge"""
return request.param
22 changes: 21 additions & 1 deletion tests/test_complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,32 @@

import networkx as nx
import tsplib95
from tspwplib.complete import is_complete, is_complete_with_self_loops
from tspwplib.utils import build_path_to_tsplib_instance


def test_is_complete():
"""Test the is complete function"""
for k in range(0, 10):
graph = nx.complete_graph(k)
assert is_complete(graph)
assert sum(range(graph.number_of_nodes())) == graph.number_of_edges()


def test_is_complete_with_self_loops():
"""Test graph is complete and has self loops"""
for k in range(1, 10):
graph = nx.complete_graph(k)
assert is_complete(graph)
assert not is_complete_with_self_loops(graph)
for u in graph:
graph.add_edge(u, u)
assert is_complete_with_self_loops(graph)


def test_tsplib_is_complete(tsplib_root, instance_name):
"""Test each instance of TSPLIB95 is a complete graph"""
filepath = build_path_to_tsplib_instance(tsplib_root, instance_name)
problem = tsplib95.load(filepath)
graph = problem.get_graph()
assert nx.complete_graph(graph)
assert is_complete_with_self_loops(graph)
44 changes: 44 additions & 0 deletions tests/test_sparsity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Tests for sparsity"""

import networkx as nx
from tsplib95.models import StandardProblem
from tspwplib.complete import is_complete_with_self_loops
from tspwplib.sparsity import remove_random_edges_from_graph
from tspwplib.utils import build_path_to_tsplib_instance


def test_remove_random_edges_from_graph(
tsplib_root, instance_name, edge_removal_probability
):
"""Test the right number of edges are removed"""
filepath = build_path_to_tsplib_instance(tsplib_root, instance_name)
problem = StandardProblem.load(filepath)
complete_graph = problem.get_graph()
assert is_complete_with_self_loops(complete_graph)
smaller_graph = remove_random_edges_from_graph(
complete_graph, edge_removal_probability=edge_removal_probability
)
# edge cases
if edge_removal_probability == 0:
assert smaller_graph.number_of_edges() == complete_graph.number_of_edges()
elif edge_removal_probability == 1.0:
assert smaller_graph.number_of_edges() == 0
# sufficient number of nodes for randomness to not have big effect
elif smaller_graph.number_of_nodes() > 10:
assert not is_complete_with_self_loops(smaller_graph)
num_edges_lower_bound = complete_graph.number_of_edges() * (
1 - edge_removal_probability - 0.1
)
num_edges_upper_bound = complete_graph.number_of_edges() * (
1 - edge_removal_probability + 0.1
)
assert (
num_edges_lower_bound
<= smaller_graph.number_of_edges()
<= num_edges_upper_bound
)
assert nx.is_connected(smaller_graph)


def test_measure_sparsity_metrics():
"""Test the sparsity of graphs is measured correctly"""
37 changes: 37 additions & 0 deletions tspwplib/complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Functions for complete graphs"""

import networkx as nx


def is_complete(G: nx.Graph) -> bool:
"""Check if the graph is complete
Args:
G: Simple graph
Returns:
True if the graph is complete, false otherwise
Note:
Assumes no self loops
"""
for u in G:
for v in G:
if not G.has_edge(u, v) and u != v:
return False
return True


def is_complete_with_self_loops(G: nx.Graph) -> bool:
"""Check if the graph is complete, and every vertex has a self loop
Args:
G: Simple graph
Returns:
True if the graph is complete, false otherwise
"""
for u in G:
if not G.has_edge(u, u):
return False
return is_complete(G)
42 changes: 42 additions & 0 deletions tspwplib/sparsity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Functions for creating and measuring the sparsity of graphs.
To calculate the k-degeneracy of a graph you can use networkx:
```python
degeneracy(G) = max(networkx.core_number(G).values())
```
"""

import random
from typing import Dict
import networkx as nx


def remove_random_edges_from_graph(
G: nx.Graph, edge_removal_probability: float = 0.5
) -> nx.Graph:
"""Remove edges from the graph to make it more sparse.
Edges are removed randomly with uniform and indepedent probability.
Args:
G: Complete graph
edge_removal_probability: Probability of removing an edge from G
Returns:
New graph with edge removed
"""
# make copy of graph to avoid editing original copy
H = G.copy()

# for each edge in G, remove in H if random number if less than edge removal prob
for u, v in G.edges():
if random.random() < edge_removal_probability:
H.remove_edge(u, v)
return H


def measure_sparsity_metrics(G: nx.Graph) -> Dict[str, float]:
"""Calculate metrics for how sparse a graph is"""
return dict(
degeneracy=max(nx.core_number(G).values()),
degree_ratio=sum(nx.degree(G).values()) / (2 * G.number_of_edges()),
)

0 comments on commit c87a1bb

Please sign in to comment.