Skip to content
16 changes: 12 additions & 4 deletions python_tsp/exact/brute_force.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

def solve_tsp_brute_force(
distance_matrix: np.ndarray,
starting_node: int = 0,
) -> Tuple[Optional[List], Any]:
"""Solve TSP to optimality with a brute force approach

Expand All @@ -18,6 +19,9 @@ def solve_tsp_brute_force(
Distance matrix of shape (n x n) with the (i, j) entry indicating the
distance from node i to j. It does not need to be symmetric

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
A permutation of nodes from 0 to n that produces the least total
Expand All @@ -33,14 +37,18 @@ def solve_tsp_brute_force(
reducing the possibilities to (n - 1)!.
"""

# Exclude 0 from the range since it is fixed as starting point
points = range(1, distance_matrix.shape[0])
# Exclude `starting_node` from the range since it is fixed
other_points = [
node
for node in range(distance_matrix.shape[0])
if node != starting_node
]
best_distance = np.inf
best_permutation = None

for partial_permutation in permutations(points):
for partial_permutation in permutations(other_points):
# Remember to add the starting node before evaluating it
permutation = [0] + list(partial_permutation)
permutation = [starting_node] + list(partial_permutation)
distance = compute_permutation_distance(distance_matrix, permutation)

if distance < best_distance:
Expand Down
22 changes: 15 additions & 7 deletions python_tsp/exact/dynamic_programming.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def solve_tsp_dynamic_programming(
distance_matrix: np.ndarray,
maxsize: Optional[int] = None,
starting_node: int = 0,
) -> Tuple[List, float]:
"""
Solve TSP to optimality with dynamic programming
Expand All @@ -22,6 +23,9 @@ def solve_tsp_dynamic_programming(
size for the recursion tree. Defaults to `None`, which essentially
means "take as much space as needed".

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
permutation
Expand Down Expand Up @@ -89,16 +93,20 @@ def solve_tsp_dynamic_programming(
---------
https://en.wikipedia.org/wiki/Held%E2%80%93Karp_algorithm#cite_note-5
"""
# Get initial set {1, 2, ..., tsp_size} as a frozenset because @lru_cache
# requires a hashable type
N = frozenset(range(1, distance_matrix.shape[0]))
# Get initial set {0, 1, 2, ..., tsp_size} \ {starting_node} as a frozenset
# because @lru_cache requires a hashable type
N = frozenset(
node
for node in range(distance_matrix.shape[0])
if node != starting_node
)
memo: Dict[Tuple, int] = {}

# Step 1: get minimum distance
@lru_cache(maxsize=maxsize)
def dist(ni: int, N: frozenset) -> float:
if not N:
return distance_matrix[ni, 0]
return distance_matrix[ni, starting_node]

# Store the costs in the form (nj, dist(nj, N))
costs = [
Expand All @@ -110,11 +118,11 @@ def dist(ni: int, N: frozenset) -> float:

return min_cost

best_distance = dist(0, N)
best_distance = dist(starting_node, N)

# Step 2: get path with the minimum distance
ni = 0 # start at the origin
solution = [0]
ni = starting_node # start at the origin
solution = [starting_node]

while N:
ni = memo[(ni, N)]
Expand Down
6 changes: 5 additions & 1 deletion python_tsp/heuristics/lin_kernighan.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def solve_tsp_lin_kernighan(
x0: Optional[List[int]] = None,
log_file: Optional[str] = None,
verbose: bool = False,
starting_node: int = 0,
) -> Tuple[List[int], float]:
"""
Solve the Traveling Salesperson Problem using the Lin-Kernighan algorithm.
Expand All @@ -163,6 +164,9 @@ def solve_tsp_lin_kernighan(
verbose
If true, prints algorithm status every iteration.

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
Tuple
Expand All @@ -174,7 +178,7 @@ def solve_tsp_lin_kernighan(
Chapter 5, Section 5.3.2.1: Lin-Kernighan Neighborhood, Springer, 2023.
"""
hamiltonian_cycle, hamiltonian_cycle_distance = setup_initial_solution(
distance_matrix=distance_matrix, x0=x0
distance_matrix=distance_matrix, x0=x0, starting_node=starting_node
)
num_vertices = distance_matrix.shape[0]
vertices = list(range(num_vertices))
Expand Down
8 changes: 7 additions & 1 deletion python_tsp/heuristics/local_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def solve_tsp_local_search(
max_processing_time: Optional[float] = None,
log_file: Optional[str] = None,
verbose: bool = False,
starting_node: int = 0,
) -> Tuple[List, float]:
"""Solve a TSP problem with a local search heuristic

Expand All @@ -47,6 +48,9 @@ def solve_tsp_local_search(
verbose
If true, prints algorithm status every iteration

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
A permutation of nodes from 0 to n - 1 that produces the least total
Expand All @@ -65,7 +69,9 @@ def solve_tsp_local_search(
3. Repeat step 2 until all neighbors of `x` are tried and there is no
improvement. Return `x`, `fx` as solution.
"""
x, fx = setup_initial_solution(distance_matrix, x0)
x, fx = setup_initial_solution(
distance_matrix, x0, starting_node=starting_node
)
max_processing_time = max_processing_time or np.inf

log_file_handler = (
Expand Down
8 changes: 7 additions & 1 deletion python_tsp/heuristics/record_to_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def solve_tsp_record_to_record(
max_iterations: Optional[int] = None,
log_file: Optional[str] = None,
verbose: bool = False,
starting_node: int = 0,
):
"""
Solve the traveling Salesperson Problem using a
Expand All @@ -48,6 +49,9 @@ def solve_tsp_record_to_record(
verbose
If true, prints algorithm status every iteration.

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
Tuple
Expand All @@ -60,7 +64,9 @@ def solve_tsp_record_to_record(
"""
n = distance_matrix.shape[0]
max_iterations = max_iterations or n
x, fx = setup_initial_solution(distance_matrix=distance_matrix, x0=x0)
x, fx = setup_initial_solution(
distance_matrix=distance_matrix, x0=x0, starting_node=starting_node
)

log_file_handler = (
open(log_file, "w", encoding="utf-8") if log_file else None
Expand Down
8 changes: 7 additions & 1 deletion python_tsp/heuristics/simulated_annealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def solve_tsp_simulated_annealing(
max_processing_time: Optional[float] = None,
log_file: Optional[str] = None,
verbose: bool = False,
starting_node: int = 0,
) -> Tuple[List, float]:
"""Solve a TSP problem using a Simulated Annealing
The approach used here is the one proposed in [1].
Expand Down Expand Up @@ -58,6 +59,9 @@ def solve_tsp_simulated_annealing(
verbose {False}
If true, prints algorithm status every iteration

starting_node
Determines the starting node of the final permutation. Defaults to 0.

Returns
-------
A permutation of nodes from 0 to n - 1 that produces the least total
Expand All @@ -71,7 +75,9 @@ def solve_tsp_simulated_annealing(
case studies. Springer Science & Business Media, 2006.
"""

x, fx = setup_initial_solution(distance_matrix, x0)
x, fx = setup_initial_solution(
distance_matrix, x0, starting_node=starting_node
)
temp = _initial_temperature(distance_matrix, x, fx, perturbation_scheme)
max_processing_time = max_processing_time or inf
log_file_handler = (
Expand Down
118 changes: 114 additions & 4 deletions python_tsp/utils/setup_initial_solution.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
from random import sample
from random import sample, choice
from typing import List, Optional, Tuple

import numpy as np

from .permutation_distance import compute_permutation_distance


DEFAULT_STARTING_NODE = 0
STARTING_NODE_OUTSIDE_BOUNDARIES_MSG = (
"Starting node outside limits [0, num_nodes]"
)
ENDING_NODE_OUTSIDE_BOUNDARIES_MSG = (
"Ending node outside limits [0, num_nodes]"
)
STARTING_ENDING_NODE_ARE_EQUAL_MSG = (
"Starting and ending nodes cannot be equal"
)
INVALID_SIZE_INITIAL_SOLUTION_MSG = (
"`x0` size does not match the number of nodes from distance matrix"
)
INVALID_INITIAL_SOLUTION_MSG = (
"`x0` does not contain a permutation of all nodes"
)
MISMATCH_INPUT_ARGUMENTS_MSG = (
"`x0` first and last node do not match with "
"`starting_node` or `ending_node`"
)


def setup_initial_solution(
distance_matrix: np.ndarray, x0: Optional[List] = None
distance_matrix: np.ndarray,
x0: Optional[List[int]] = None,
starting_node: Optional[int] = DEFAULT_STARTING_NODE,
ending_node: Optional[int] = None,
) -> Tuple[List[int], float]:
"""Return initial solution and its objective value

Expand All @@ -21,6 +46,14 @@ def setup_initial_solution(
Permutation of nodes from 0 to n - 1 indicating the starting solution.
If not provided, a random list is created.

starting_node
First node to appear in the permutation.

ending_node
Last node to appear in the permutation. Note, despite the name, the
TSP is still by definition a cycle, so we always go back from
`ending_node` to `starting_node`.

Returns
-------
x0
Expand All @@ -30,10 +63,87 @@ def setup_initial_solution(
fx0
Objective value of x0
"""
_validate_input_arguments(distance_matrix, x0, starting_node, ending_node)

if not x0:
n = distance_matrix.shape[0] # number of nodes
x0 = [0] + sample(range(1, n), n - 1) # ensure 0 is the first node
num_nodes = distance_matrix.shape[0]
x0 = _build_initial_permutation(
num_nodes, starting_node=starting_node, ending_node=ending_node
)

fx0 = compute_permutation_distance(distance_matrix, x0)
return x0, fx0


def _validate_input_arguments(
distance_matrix: np.ndarray,
x0: Optional[List[int]],
starting_node: Optional[int],
ending_node: Optional[int],
) -> None:
"""Validate combination of input parameters"""
num_nodes = distance_matrix.shape[0]

if x0:
all_nodes = set(range(num_nodes))
if len(x0) != num_nodes:
raise ValueError(INVALID_SIZE_INITIAL_SOLUTION_MSG)
if set(x0) != all_nodes:
raise ValueError(INVALID_INITIAL_SOLUTION_MSG)
if starting_node is not None and x0[0] != starting_node:
raise ValueError(MISMATCH_INPUT_ARGUMENTS_MSG)
if ending_node is not None and x0[-1] != ending_node:
raise ValueError(MISMATCH_INPUT_ARGUMENTS_MSG)

if starting_node is not None:
if starting_node < 0 or starting_node >= num_nodes:
raise ValueError(STARTING_NODE_OUTSIDE_BOUNDARIES_MSG)

if ending_node is not None:
if ending_node < 0 or ending_node >= num_nodes:
raise ValueError(ENDING_NODE_OUTSIDE_BOUNDARIES_MSG)

if starting_node is not None and ending_node is not None:
if starting_node == ending_node:
raise ValueError(STARTING_ENDING_NODE_ARE_EQUAL_MSG)


def _build_initial_permutation(
num_nodes: int,
starting_node: Optional[int] = DEFAULT_STARTING_NODE,
ending_node: Optional[int] = None,
) -> List[int]:
"""
Build a random list of integers from 0 to `num_nodes` - 1 guaranteeing the
initial node is `starting_node` and the last one is `ending_node`.

If not provided, `starting_node` is assumed as 0 and `ending_node` is taken
randomly.
"""

all_nodes = list(range(num_nodes))
starting_node = (
starting_node if starting_node is not None else choice(all_nodes)
)

all_nodes_except_starting_node = [
node for node in range(num_nodes) if node != starting_node
]
ending_node = (
ending_node
if ending_node is not None
else choice(all_nodes_except_starting_node)
)

all_nodes_without_extremes = [
node
for node in range(num_nodes)
if node != starting_node and node != ending_node
]
x0 = (
[starting_node]
+ sample(all_nodes_without_extremes, len(all_nodes_without_extremes))
+ [ending_node]
)

return x0
Loading