diff --git a/.pylintrc b/.pylintrc index eb466f7..e003169 100644 --- a/.pylintrc +++ b/.pylintrc @@ -427,6 +427,7 @@ good-names=i, v, w, G, + H, T, x, y, diff --git a/tests/conftest.py b/tests/conftest.py index 66f26cd..b52ef19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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( @@ -24,6 +27,9 @@ def pytest_addoption(parser): ) +# fixtures for filepaths + + @pytest.fixture(scope="function") def tsplib_root(request) -> Path: """Root of tsplib95 data""" @@ -36,6 +42,9 @@ def oplib_root(request) -> Path: return Path(request.config.getoption("--oplib-root")) +# fixtures for types + + @pytest.fixture( scope="function", params=[ @@ -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 diff --git a/tests/test_complete.py b/tests/test_complete.py index cde5908..a0aa29b 100644 --- a/tests/test_complete.py +++ b/tests/test_complete.py @@ -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) diff --git a/tests/test_sparsity.py b/tests/test_sparsity.py new file mode 100644 index 0000000..24d1716 --- /dev/null +++ b/tests/test_sparsity.py @@ -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""" diff --git a/tspwplib/complete.py b/tspwplib/complete.py new file mode 100644 index 0000000..177a05a --- /dev/null +++ b/tspwplib/complete.py @@ -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) diff --git a/tspwplib/sparsity.py b/tspwplib/sparsity.py new file mode 100644 index 0000000..8603ac8 --- /dev/null +++ b/tspwplib/sparsity.py @@ -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()), + )