diff --git a/tests/test_complete.py b/tests/test_complete.py index 570bf8a..cde5908 100644 --- a/tests/test_complete.py +++ b/tests/test_complete.py @@ -1,11 +1,13 @@ """Test that all existing instances are complete graphs""" import networkx as nx -from tspwplib.dataset import load_tsplib_dataset +import tsplib95 +from tspwplib.utils import build_path_to_tsplib_instance def test_tsplib_is_complete(tsplib_root, instance_name): """Test each instance of TSPLIB95 is a complete graph""" - problem = load_tsplib_dataset(tsplib_root, instance_name) + filepath = build_path_to_tsplib_instance(tsplib_root, instance_name) + problem = tsplib95.load(filepath) graph = problem.get_graph() assert nx.complete_graph(graph) diff --git a/tests/test_profits_problem.py b/tests/test_profits_problem.py new file mode 100644 index 0000000..ced1f98 --- /dev/null +++ b/tests/test_profits_problem.py @@ -0,0 +1,32 @@ +"""Test the TSP with Profits Problem class""" + +import math +import networkx as nx +from tspwplib.problem import ProfitsProblem +from tspwplib.types import OptimalSolutionTSP +from tspwplib.utils import build_path_to_oplib_instance + + +def test_parse_profits_problem(oplib_root, generation, instance_name, alpha): + """Test an OP instance can be parsed""" + filepath = build_path_to_oplib_instance( + oplib_root, generation, instance_name, alpha=alpha + ) + assert "COST_LIMIT" in ProfitsProblem.fields_by_keyword + + problem = ProfitsProblem.load(filepath) + assert problem.is_complete() + graph = problem.get_graph() + assert nx.complete_graph(graph) + + +def test_get_cost_limit(oplib_root, generation, instance_name, alpha): + """Test we can get the cost limit""" + filepath = build_path_to_oplib_instance( + oplib_root, generation, instance_name, alpha=alpha + ) + problem = ProfitsProblem.load(filepath) + expected_cost_limit = math.ceil( + OptimalSolutionTSP[instance_name] * (alpha.value / 100.0) + ) + assert problem.get_cost_limit() == expected_cost_limit diff --git a/tspwplib/dataset.py b/tspwplib/dataset.py deleted file mode 100644 index 7f57507..0000000 --- a/tspwplib/dataset.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Functions and classes for datasets""" - -from pathlib import Path -import tsplib95 -from .types import InstanceName - - -def load_tsplib_dataset(root: Path, name: InstanceName): - """Load a TSP lib problem""" - problem = tsplib95.load(root / (name.value + ".tsp")) - return problem diff --git a/tspwplib/problem.py b/tspwplib/problem.py new file mode 100644 index 0000000..02b0ea6 --- /dev/null +++ b/tspwplib/problem.py @@ -0,0 +1,38 @@ +"""Functions and classes for datasets""" + +from typing import Dict +import tsplib95 + + +class ProfitsProblem(tsplib95.models.StandardProblem): + """TSP with Profits Problem""" + + # Maximum distance of the total route in a OP. + cost_limit = tsplib95.fields.IntegerField("COST_LIMIT") + # The scores of the nodes of a OP are given in the form (per line) + node_score = tsplib95.fields.DemandsField("NODE_SCORE_SECTION") + # The optimal solution to the TSP + tspsol = tsplib95.fields.IntegerField("TSPSOL") + + def get_cost_limit(self) -> int: + """Get the cost limit for a TSP with Profits problem + + Returns: + Cost limit + """ + return self.cost_limit + + def get_node_score(self) -> Dict[int, int]: + """Get the node scores (profits) + + Returns: + Mapping from node to node score (profit) + """ + + def get_tsp_optimal_value(self) -> int: + """Get the value of the optimal solution to TSP + + Returns: + TSP optimal value + """ + return self.tspsol diff --git a/tspwplib/types.py b/tspwplib/types.py index ff41ba0..e71bd15 100644 --- a/tspwplib/types.py +++ b/tspwplib/types.py @@ -1,6 +1,6 @@ """Type hinting and names""" -from enum import Enum +from enum import Enum, IntEnum class InstanceName(str, Enum): @@ -25,3 +25,120 @@ class Alpha(Enum): """Ratio between profit/cost limit and profit/cost of TSP solution""" fifty = 50 + + +class OptimalSolutionTSP(IntEnum): + """Value of optimal solutions to TSP instances""" + + a280 = 2579 + ali535 = 202339 + att48 = 10628 + att532 = 27686 + bayg29 = 1610 + bays29 = 2020 + berlin52 = 7542 + bier127 = 118282 + brazil58 = 25395 + brd14051 = 469385 + brg180 = 1950 + burma14 = 3323 + ch130 = 6110 + ch150 = 6528 + d198 = 15780 + d493 = 35002 + d657 = 48912 + d1291 = 50801 + d1655 = 62128 + d2103 = 80450 + d15112 = 1573084 + d18512 = 645238 + dantzig42 = 699 + dsj1000 = 18659688 # (EUC_2D) + # dsj1000 = 18660188 # (CEIL_2D) # NOTE breaks keys + eil51 = 426 + eil76 = 538 + eil101 = 629 + fl417 = 11861 + fl1400 = 20127 + fl1577 = 22249 + fl3795 = 28772 + fnl4461 = 182566 + fri26 = 937 + gil262 = 2378 + gr17 = 2085 + gr21 = 2707 + gr24 = 1272 + gr48 = 5046 + gr96 = 55209 + gr120 = 6942 + gr137 = 69853 + gr202 = 40160 + gr229 = 134602 + gr431 = 171414 + gr666 = 294358 + hk48 = 11461 + kroA100 = 21282 + kroB100 = 22141 + kroC100 = 20749 + kroD100 = 21294 + kroE100 = 22068 + kroA150 = 26524 + kroB150 = 26130 + kroA200 = 29368 + kroB200 = 29437 + lin105 = 14379 + lin318 = 42029 + linhp318 = 41345 + nrw1379 = 56638 + p654 = 34643 + pa561 = 2763 + pcb442 = 50778 + pcb1173 = 56892 + pcb3038 = 137694 + pla7397 = 23260728 + pla33810 = 66048945 + pla85900 = 142382641 + pr76 = 108159 + pr107 = 44303 + pr124 = 59030 + pr136 = 96772 + pr144 = 58537 + pr152 = 73682 + pr226 = 80369 + pr264 = 49135 + pr299 = 48191 + pr439 = 107217 + pr1002 = 259045 + pr2392 = 378032 + rat99 = 1211 + rat195 = 2323 + rat575 = 6773 + rat783 = 8806 + rd100 = 7910 + rd400 = 15281 + rl1304 = 252948 + rl1323 = 270199 + rl1889 = 316536 + rl5915 = 565530 + rl5934 = 556045 + rl11849 = 923288 + si175 = 21407 + si535 = 48450 + si1032 = 92650 + st70 = 675 + swiss42 = 1273 + ts225 = 126643 + tsp225 = 3916 + u159 = 42080 + u574 = 36905 + u724 = 41910 + u1060 = 224094 + u1432 = 152970 + u1817 = 57201 + u2152 = 64253 + u2319 = 234256 + ulysses16 = 6859 + ulysses22 = 7013 + usa13509 = 19982859 + vm1084 = 239297 + vm1748 = 336556