From 10301046a7b10d6a474641c6cc101f3cc08e864b Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Fri, 15 Oct 2021 13:22:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Test=20loading=20from=20tsplib95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_base_tsp.py | 18 +++++++++++++ tspwplib/__init__.py | 13 +++++++++- tspwplib/problem.py | 59 +++++++++++++++++++++++++++++++++++------- tspwplib/types.py | 14 +++++----- 4 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 tests/test_base_tsp.py diff --git a/tests/test_base_tsp.py b/tests/test_base_tsp.py new file mode 100644 index 0000000..0933649 --- /dev/null +++ b/tests/test_base_tsp.py @@ -0,0 +1,18 @@ +"""Tests for the pydantic representation of a TSP""" + +import pytest +from tsplib95.models import StandardProblem +from tspwplib import BaseTSP, GraphName, build_path_to_tsplib_instance + + +@pytest.mark.parametrize("gname", list(GraphName)) +def test_from_tsplib95(tsplib_root, gname): + """Test tsplib95 problems can be read into BaseTSP""" + # only load problems with less than 1000 vertices + n_nodes = int("".join(filter(str.isdigit, gname.value))) + if n_nodes < 1000: + tsp_path = build_path_to_tsplib_instance(tsplib_root, gname) + assert tsp_path.exists() + problem = StandardProblem.load(tsp_path) + tsp = BaseTSP.from_tsplib95(problem) + assert len(tsp.edge_data) == len(list(problem.get_edges())) diff --git a/tspwplib/__init__.py b/tspwplib/__init__.py index b92e07c..1deb36a 100644 --- a/tspwplib/__init__.py +++ b/tspwplib/__init__.py @@ -14,6 +14,7 @@ split_head, split_tail, tail_prize, + to_simple_undirected, to_vertex_dataframe, ) from .complete import is_complete, is_complete_with_self_loops @@ -23,7 +24,7 @@ NotSimpleCycleException, NotSimplePathException, ) -from .problem import ProfitsProblem, is_pctsp_yes_instance +from .problem import BaseTSP, ProfitsProblem, is_pctsp_yes_instance from .utils import build_path_to_oplib_instance, build_path_to_tsplib_instance from .types import ( Alpha, @@ -34,6 +35,10 @@ EdgeList, Generation, GraphName, + LondonaqGraphName, + LondonaqLocation, + LondonaqLocationShort, + LondonaqTimestamp, OptimalSolutionTSP, Vertex, VertexFunction, @@ -58,6 +63,7 @@ __all__ = [ "Alpha", + "BaseTSP", "DisjointPaths", "Edge", "EdgeFunction", @@ -66,6 +72,10 @@ "EdgesNotAdjacentException", "Generation", "GraphName", + "LondonaqGraphName", + "LondonaqLocation", + "LondonaqLocationShort", + "LondonaqTimestamp", "NotSimpleException", "NotSimpleCycleException", "NotSimplePathException", @@ -104,6 +114,7 @@ "split_head", "split_tail", "tail_prize", + "to_simple_undirected", "to_vertex_dataframe", "total_cost", "total_cost_networkx", diff --git a/tspwplib/problem.py b/tspwplib/problem.py index 3f5eaf1..c7a1dd1 100644 --- a/tspwplib/problem.py +++ b/tspwplib/problem.py @@ -51,7 +51,7 @@ class BaseTSP(pydantic.BaseModel): edge_weight_type: EdgeWeightType fixed_edges: EdgeList name: str - node_coords: NodeCoords + node_coords: Optional[NodeCoords] node_coord_type: NodeCoordType problem_type: str tours: Optional[List[VertexList]] @@ -217,25 +217,66 @@ def from_dataframes( @classmethod def from_tsplib95(cls, problem: tsplib95.models.StandardProblem): """Get a TSP base model from a StandardProblem object""" + + display_data_type = ( + problem.display_data_type + if problem.display_data_type + else DisplayDataType.NO_DISPLAY + ) + edge_data_format = ( + problem.edge_data_format + if problem.edge_data_format + else EdgeDataFormat.EDGE_LIST + ) + edge_weight_type = problem.edge_weight_type + + # edge weight format + edge_weight_format = problem.edge_weight_format + if ( + not edge_weight_format + and edge_weight_type in EdgeWeightType.__members__ + and edge_weight_type != EdgeWeightType.EXPLICIT + ): + edge_weight_format = EdgeWeightFormat.FUNCTION + elif not edge_weight_format and edge_weight_type == EdgeWeightType.EXPLICIT: + raise ValueError( + "Edge weight type is set to EXPLICIT but no edge weight format is given" + ) + elif not edge_weight_format: + raise ValueError( + "Edge weight format in StandardProblem is not set - cannot assign edge weights." + ) + + node_coord_type = ( + problem.node_coord_type + if problem.node_coord_type + else NodeCoordType.NO_COORDS + ) + node_coords = None + if node_coord_type == NodeCoordType.TWOD_COORDS: + node_coords = {i: problem.node_coords.get(i) for i in problem.get_nodes()} + elif node_coord_type == NodeCoordType.THREED_COORDS: + raise NotImplementedError("3D coords not yet supported") + return cls( capacity=problem.capacity, - comment=problem.comment, + comment=problem.comment if problem.comment else "", demands=problem.demands, depots=problem.depots, dimension=problem.dimension, display_data=problem.display_data, - display_data_type=problem.display_data_type, - edge_data=problem.get_edges(), - edge_data_format=problem.edge_data_format, + display_data_type=display_data_type, + edge_data=list(problem.get_edges()), + edge_data_format=edge_data_format, edge_weights={ (i, j): problem.get_weight(i, j) for i, j in problem.get_edges() }, - edge_weight_format=problem.edge_weight_format, - edge_weight_type=problem.edge_weight_type, + edge_weight_format=edge_weight_format, + edge_weight_type=edge_weight_type, fixed_edges=problem.fixed_edges, name=problem.name, - node_coords=[problem.node_coords.get(i) for i in problem.get_nodes()], - node_coord_type=problem.node_coord_type, + node_coords=node_coords, + node_coord_type=node_coord_type, problem_type=problem.type, tours=problem.tours, ) diff --git a/tspwplib/types.py b/tspwplib/types.py index 0c0409c..3738412 100644 --- a/tspwplib/types.py +++ b/tspwplib/types.py @@ -38,13 +38,13 @@ class EdgeWeightType(StrEnumMixin, str, Enum): """Specifies how the edge weights (or distances) are given""" EXPLICIT = "EXPLICIT" # Weights are listed explicitly in the corresponding section - EUC2D = "EUC2D" # Weights are Euclidean distances in 2-D - EUC3D = "EUC3D" # Weights are Euclidean distances in 3-D - MAX2D = "MAX2D" # Weights are maximum distances in 2-D - MAX3D = "MAX3D" # Weights are maximum distances in 3-D - MAN2D = "MAN2D" # Weights are Manhattan distances in 2-D - MAN3D = "MAN3D" # Weights are Manhattan distances in 3-D - CEIL2D = "CEIL2D" # Weights are Euclidean distances in 2-D rounded up + EUC_2D = "EUC_2D" # Weights are Euclidean distances in 2-D + EUC_3D = "EUC_3D" # Weights are Euclidean distances in 3-D + MAX_2D = "MAX_2D" # Weights are maximum distances in 2-D + MAX_3D = "MAX_3D" # Weights are maximum distances in 3-D + MAN_2D = "MAN_2D" # Weights are Manhattan distances in 2-D + MAN_3D = "MAN_3D" # Weights are Manhattan distances in 3-D + CEIL_2D = "CEIL_2D" # Weights are Euclidean distances in 2-D rounded up GEO = "GEO" # Weights are geographical distances ATT = "ATT" # Special distance function for problems att48 and att532 XRAY1 = (