From 2944eb5b1048dec1a1e1d19484c0a188d4dff0ca Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 11 Jan 2021 17:55:31 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9E=95=20Install=20graph=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 9 +++++++++ setup.py | 1 + 2 files changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4e00c61..9335069 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,15 @@ python: - "3.6" - "3.8" cache: pip +addons: + apt: + update: true + sources: + # add the graph-tool source for apt-get + - sourceline: "deb [ arch=amd64 ] https://downloads.skewed.de/apt focal main" + key_url: https://keys.openpgp.org/vks/v1/by-fingerprint/793CEFE14DBC851A2BFB1222612DEFB798507F25 + packages: + - python3-graph-tool env: - TSPLIB_ROOT="../tsplib95/archives/problems/tsp" OPLIB_ROOT="../OPLib/" before_install: diff --git a/setup.py b/setup.py index a8eb073..9213a4e 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ install_requires=[ "pandas>=1.0.0", "tsplib95", + "graph_tool>=2.37", ], name="tspwplib", packages=["tspwplib"], From 88335f37aad2d7f6b1fcee081cd4d1d97b5c8906 Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 11 Jan 2021 18:06:10 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=92=9A=20Fix=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 23 ++++++++++++++--------- tspwplib/problem.py | 9 +++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9335069..dc0ec54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,21 +4,26 @@ python: - "3.6" - "3.8" cache: pip -addons: - apt: - update: true - sources: - # add the graph-tool source for apt-get - - sourceline: "deb [ arch=amd64 ] https://downloads.skewed.de/apt focal main" - key_url: https://keys.openpgp.org/vks/v1/by-fingerprint/793CEFE14DBC851A2BFB1222612DEFB798507F25 - packages: - - python3-graph-tool env: - TSPLIB_ROOT="../tsplib95/archives/problems/tsp" OPLIB_ROOT="../OPLib/" before_install: - git clone https://github.com/bcamath-ds/OPLib.git ../OPLib - git clone https://github.com/rhgrant10/tsplib95.git ../tsplib95 install: + # install conda + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + - bash miniconda.sh -b -p $HOME/miniconda + - source "$HOME/miniconda/etc/profile.d/conda.sh" + - hash -r + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda + - conda info -a + # create conda env and activate + - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION + - conda activate test-environment + # install graph tool + - conda install -c conda-forge graph-tool + # install our package - pip install . - pip install -r requirements.txt script: diff --git a/tspwplib/problem.py b/tspwplib/problem.py index 53d2602..6665b58 100644 --- a/tspwplib/problem.py +++ b/tspwplib/problem.py @@ -1,6 +1,7 @@ """Functions and classes for datasets""" from typing import List +import graph_tool as gt import networkx as nx import tsplib95 from .types import Vertex, VertexFunctionName, VertexLookup @@ -82,6 +83,14 @@ def get_graph(self, normalize: bool = False) -> nx.Graph: self.__set_edge_attributes(graph, names) return graph + def get_graph_tool(self, normalize: bool = False) -> gt.Graph: + """Return a graph tools undirected graph + + Args: + normalize: rename nodes to be zero-indexed + """ + pass + def get_cost_limit(self) -> int: """Get the cost limit for a TSP with Profits problem From 1809ba6c468c9dbee9fdb01782b284c7488e3278 Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 11 Jan 2021 19:52:57 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20New=20function=20for=20passing?= =?UTF-8?q?=20graph=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mypy.ini | 3 ++ tests/test_profits_problem.py | 33 +++++++++++++++++ tspwplib/problem.py | 67 ++++++++++++++++++++++++++++++----- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/.mypy.ini b/.mypy.ini index 1abcf2c..0c16ac8 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,5 +1,8 @@ [mypy] +[mypy-graph_tool.*] +ignore_missing_imports=True + [mypy-networkx.*] ignore_missing_imports = True diff --git a/tests/test_profits_problem.py b/tests/test_profits_problem.py index eb8647c..1192ca5 100644 --- a/tests/test_profits_problem.py +++ b/tests/test_profits_problem.py @@ -81,3 +81,36 @@ def test_get_graph(oplib_root, generation, graph_name, alpha): assert type(value) in valid_types assert graph.graph["root"] == 1 + + +def test_get_graph_tool(oplib_root, generation, graph_name, alpha): + """Test returning graph tool undirected weighted graph""" + filepath = build_path_to_oplib_instance( + oplib_root, generation, graph_name, alpha=alpha + ) + problem = ProfitsProblem.load(filepath) + gt_graph = problem.get_graph_tool() + nx_graph = problem.get_graph(normalize=True) + assert nx_graph.has_node(0) + assert 0 in gt_graph.get_vertices() + assert gt_graph.num_vertices() == nx_graph.number_of_nodes() + assert gt_graph.num_edges() == nx_graph.number_of_edges() + + # check weight + for u, v, data in nx_graph.edges(data=True): + gt_edge = gt_graph.edge(u, v, add_missing=False) + assert gt_edge + assert gt_graph.ep.weight[gt_edge] == data["weight"] + # check prize on vertices + for u, data in nx_graph.nodes(data=True): + assert data["prize"] == gt_graph.vertex_properties.prize[u] + + +def test_get_root_vertex(oplib_root, generation, graph_name, alpha): + """Test the root vertex is 1 when un-normalized (0 when normalized)""" + filepath = build_path_to_oplib_instance( + oplib_root, generation, graph_name, alpha=alpha + ) + problem = ProfitsProblem.load(filepath) + assert problem.get_root_vertex(normalize=False) == 1 + assert problem.get_root_vertex(normalize=True) == 0 diff --git a/tspwplib/problem.py b/tspwplib/problem.py index 6665b58..dfa65d0 100644 --- a/tspwplib/problem.py +++ b/tspwplib/problem.py @@ -4,7 +4,7 @@ import graph_tool as gt import networkx as nx import tsplib95 -from .types import Vertex, VertexFunctionName, VertexLookup +from .types import Vertex, VertexLookup class ProfitsProblem(tsplib95.models.StandardProblem): @@ -33,11 +33,11 @@ def __set_graph_attributes(self, graph: nx.Graph) -> None: graph.graph["type"] = self.type graph.graph["dimension"] = self.dimension graph.graph["capacity"] = self.capacity - # pylint: disable=unsubscriptable-object - graph.graph["root"] = self.depots[0] + graph.graph["root"] = self.get_root_vertex() def __set_node_attributes(self, graph: nx.Graph, names: VertexLookup) -> None: """Add node attributes""" + node_score = self.get_node_score() for vertex in list(self.get_nodes()): # NOTE pyintergraph cannot handle bool, so we remove some attributes: # is_depot, demand, display @@ -48,6 +48,7 @@ def __set_node_attributes(self, graph: nx.Graph, names: VertexLookup) -> None: names[vertex], x=coord[0], y=coord[1], + prize=node_score[vertex], # is_depot=is_depot, ) # demand: int = self.demands.get(vertex) @@ -56,9 +57,9 @@ def __set_node_attributes(self, graph: nx.Graph, names: VertexLookup) -> None: # graph[vertex]["demand"] = demand # if not display is None: # graph[vertex]["display"] = display - nx.set_node_attributes( - graph, self.get_node_score(), name=VertexFunctionName.prize - ) + # nx.set_node_attributes( + # graph, self.get_node_score(), name=VertexFunctionName.prize + # ) def get_graph(self, normalize: bool = False) -> nx.Graph: """Return a networkx graph instance representing the problem. @@ -83,13 +84,40 @@ def get_graph(self, normalize: bool = False) -> nx.Graph: self.__set_edge_attributes(graph, names) return graph - def get_graph_tool(self, normalize: bool = False) -> gt.Graph: + def get_graph_tool(self, normalize: bool = True) -> gt.Graph: """Return a graph tools undirected graph Args: normalize: rename nodes to be zero-indexed """ - pass + graph = gt.Graph(directed=not self.is_symmetric()) + + # by default normalize because graph tools index starts at zero + nodes: List[Vertex] = list(self.get_nodes()) + if normalize: + names = {n: i for i, n in enumerate(nodes)} + else: + names = {n: n for n in nodes} + + # create list of edges + edges = [] + for u, v in self.get_edges(): + if u <= v or not self.is_symmetric(): + edges.append((names[u], names[v], self.get_weight(u, v))) + + # assign weight to edges + weight_property = graph.new_edge_property("int") + graph.add_edge_list(edges, eprops=[weight_property]) + graph.ep.weight = weight_property + + # assign prize to vertices + prize_property = graph.new_vertex_property("int") + node_score = self.get_node_score() + prize_list = [node_score[v + 1] for v in graph.get_vertices()] + prize_property.a = prize_list + graph.vertex_properties.prize = prize_property + + return graph def get_cost_limit(self) -> int: """Get the cost limit for a TSP with Profits problem @@ -114,3 +142,26 @@ def get_tsp_optimal_value(self) -> int: TSP optimal value """ return self.tspsol + + def get_root_vertex(self, normalize: bool = False) -> Vertex: + """Get the root vertex + + Args: + normalize: If true, vertices start at index 0 + + Returns: + The first depot in the list + + Raises: + ValueError: If the list of depots is empty + """ + nodes: List[Vertex] = list(self.get_nodes()) + if normalize: + names = {n: i for i, n in enumerate(nodes)} + else: + names = {n: n for n in nodes} + try: + # pylint: disable=unsubscriptable-object + return names[self.depots[0]] + except KeyError as key_error: + raise ValueError("The list of depots is empty") from key_error From 4716bb666e75530fa0b246b58c92757ac6d22fbb Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 11 Jan 2021 19:57:03 +0000 Subject: [PATCH 4/4] Remove graph_tool dependency --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9213a4e..4259e35 100644 --- a/setup.py +++ b/setup.py @@ -9,8 +9,7 @@ description="Library of instances for TSP with Profits", install_requires=[ "pandas>=1.0.0", - "tsplib95", - "graph_tool>=2.37", + "tsplib95>=0.7.1", ], name="tspwplib", packages=["tspwplib"],