From 5a7ae0cb39b0b130aa543be88e38d7b6f586cd11 Mon Sep 17 00:00:00 2001 From: Spill-Tea Date: Tue, 30 Dec 2025 14:08:35 -0800 Subject: [PATCH] chore(strategies): Improve unit testing and linting of strategies. --- src/Annealing/strategies.py | 20 ++++++++--------- tests/unit/test_strategies.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/Annealing/strategies.py b/src/Annealing/strategies.py index f5deb99..ae143de 100644 --- a/src/Annealing/strategies.py +++ b/src/Annealing/strategies.py @@ -225,7 +225,7 @@ def nearest_neighbor(self, idx: int) -> list[int]: return self.tour def minimize(self) -> tuple[list[int], float]: - min_cost: float = float("inf") + cost: float = float("inf") best_tour: list[int] = [] start: int func: Callable[[int], list[int]] @@ -234,13 +234,13 @@ def minimize(self) -> tuple[list[int], float]: for func in (self.nearest_neighbor, self.radial_nn): tour: list[int] = func(start) tour_cost: float = self.calculate_tour_cost(tour) - if tour_cost < min_cost: - min_cost = tour_cost + if tour_cost < cost: + cost = tour_cost best_tour = tour self.tour = best_tour - return self.tour, min_cost + return self.tour, cost class TwoOptStrategy(TSPStrategy): @@ -312,9 +312,9 @@ def two_opt(self, iterations: int = 1000) -> None: def minimize(self, iterations: int = 10_000) -> tuple[list[int], float]: self.two_opt(iterations) - min_cost: float = self.calculate_tour_cost(self.tour) + cost: float = self.calculate_tour_cost(self.tour) - return self.tour, min_cost + return self.tour, cost class ThreeOptStrategy(TSPStrategy): @@ -421,9 +421,9 @@ def three_opt(self, iterations: int = 1000) -> None: def minimize(self, iterations: int = 10_000) -> tuple[list[int], float]: self.three_opt(iterations) - min_cost = self.calculate_tour_cost(self.tour) + cost: float = self.calculate_tour_cost(self.tour) - return self.tour, min_cost + return self.tour, cost class ChristofidesStrategy(TSPStrategy): @@ -457,7 +457,7 @@ def christofides_tsp(self) -> list[int]: return tour def minimize(self) -> tuple[list[int], float]: - self.tour = self.christofides_tsp() - cost = self.calculate_tour_cost(self.tour) + self.tour: list[int] = self.christofides_tsp() + cost: float = self.calculate_tour_cost(self.tour) return self.tour, cost diff --git a/tests/unit/test_strategies.py b/tests/unit/test_strategies.py index 2c284cd..91ebfb2 100644 --- a/tests/unit/test_strategies.py +++ b/tests/unit/test_strategies.py @@ -37,6 +37,7 @@ def dm(coord: np.ndarray) -> np.ndarray: @pytest.fixture def tour_nn() -> list[int]: + """Nearest neighbor tour.""" return [ 8, 29, @@ -93,6 +94,7 @@ def tour_nn() -> list[int]: @pytest.fixture def tour_2opt() -> list[int]: + """Two opt tour.""" return [ 0, 21, @@ -149,6 +151,7 @@ def tour_2opt() -> list[int]: @pytest.fixture def tour_3opt() -> list[int]: + """Three opt tour.""" return [ 0, 6, @@ -260,6 +263,44 @@ def tour_cfs() -> list[int]: ] +@pytest.fixture +def sample_coord() -> np.ndarray: + coordinates: np.ndarray = np.asarray([[1, 2], [3, 4], [5, 6]]) + return coordinates + + +@pytest.fixture +def sample_graph(sample_coord: np.ndarray) -> nx.Graph: + graph: nx.Graph = nx.Graph() + graph.add_node(0, coordinate=sample_coord[0].tolist()) + graph.add_node(1, coordinate=sample_coord[1].tolist()) + graph.add_node(2, coordinate=sample_coord[2].tolist()) + graph.add_edge(0, 1, weight=strategies._euclidean(sample_coord[0], sample_coord[1])) + graph.add_edge(0, 2, weight=strategies._euclidean(sample_coord[0], sample_coord[2])) + graph.add_edge(1, 2, weight=strategies._euclidean(sample_coord[1], sample_coord[2])) + + return graph + + +def test_graph_build_from_coordinates( + sample_coord: np.ndarray, + sample_graph: nx.Graph, +) -> None: + """Confirm graph is built correctly from coordinates.""" + result: nx.Graph = strategies.build_graph(sample_coord) + assert nx.algorithms.is_isomorphic(result, sample_graph), ( + "Graphs are not isomorphic" + ) + + # convert array to list for quick python object equality checks. + for i in result.nodes: + result.nodes[i]["coordinate"] = result.nodes[i]["coordinate"].tolist() + + # NOTE: utility function from networkx only performs python object equivalence + # and numpy arrays must use np.all for complete equivalency. + assert nx.utils.graphs_equal(sample_graph, result), "Graphs are not equivalent." + + def test_matrix_to_graph(dm: np.ndarray, coord: np.ndarray) -> None: """Confirm graphs constructed are equal.""" graph_a = strategies.build_graph_from_2d_distance_matrix(dm)