Source code for QHyper.problems.tsp

# This work was supported by the EuroHPC PL infrastructure funded at the
# Smart Growth Operational Programme (2014-2020), Measure 4.2
# under the grant agreement no. POIR.04.02.00-00-D014/20-00


import itertools

import sympy
import numpy as np

from typing import cast
from QHyper.constraint import Constraint

from QHyper.parser import from_sympy
from QHyper.polynomial import Polynomial
from QHyper.problems.base import Problem


class TSP:
    """Traveling Salesman Problem

    Attributes
    ----------
    number_of_cities : int
        cities count
    cities_coords : list[tuple[float, float]]
        coordinates of the cities
    distance_matrix : list[list[float]]
        matrix of distances between cities
    normalized_distance_matrix : list[list[float]]
        normalized to (0, 1] matrix of distances between cities
    """

    def __init__(
        self,
        number_of_cities: int,
        coords_range: tuple[int, int] = (0, 10000),
        cities_coords: list[tuple[float, float]] = [],
    ) -> None:
        """
        Parameters
        ----------
        number_of_cities : int
            cities count
        coords_range : tuple[float, float]
            range of coordinates of the cities
        cities_coords : list[tuple[float, float]]
            predefined coordinates of the cities
        """
        self.number_of_cities: int = number_of_cities
        if cities_coords:
            self.cities_coords = cities_coords
        else:
            self.cities_coords = self.get_cities(coords_range)
        self.distance_matrix: list[
            list[float]] = self.calculate_distance_matrix()
        self.normalized_distance_matrix: list[
            list[float]] = self.normalize_distance_matrix()

    def get_cities(self, coords_range) -> list[tuple[float, float]]:
        cities_coords = np.random.randint(
            coords_range[0], coords_range[1],
            size=(self.number_of_cities, 2))
        return cast(list[tuple[float, float]], cities_coords)

    def calculate_distance_between_points(
        self, point_A: tuple[float, float], point_B: tuple[float, float]
    ) -> float:
        return cast(
            float,
            np.sqrt((point_A[0] - point_B[0]) ** 2
                    + (point_A[1] - point_B[1]) ** 2)
        )

    def calculate_distance_matrix(self) -> list[list[float]]:
        distance_matrix = np.zeros(
            (self.number_of_cities, self.number_of_cities))
        for i in range(self.number_of_cities):
            for j in range(i, self.number_of_cities):
                distance_matrix[i][j] = self.calculate_distance_between_points(
                    self.cities_coords[i], self.cities_coords[j])
                distance_matrix[j][i] = distance_matrix[i][j]
        return cast(list[list[float]], distance_matrix)

    def normalize_distance_matrix(self) -> list[list[float]]:
        return cast(
            list[list[float]],
            np.divide(self.distance_matrix, np.max(self.distance_matrix))
        )


[docs] class TravelingSalesmanProblem(Problem): """ Class defining objective function and constraints for TSP Parameters ---------- number_of_cities : int Number of cities cities_coords : list[tuple[float, float]], default [] List of cities coordinates. If not provided, random cities coordinates are generated. Attributes ---------- objective_function : Polynomial Objective function represented as a Polynomial constraints : list[Polynomial] List of constraints represented as a Polynomials tsp_instance: :py:class:`TSP` TSP problem instace """ def __init__( self, number_of_cities: int, cities_coords: list[tuple[float, float]] = [], ) -> None: self.tsp_instance = TSP( number_of_cities, cities_coords=cities_coords) self.variables: tuple[sympy.Symbol] = sympy.symbols( ' '.join([f'x{i}' for i in range(number_of_cities ** 2)])) self.objective_function = self._get_objective_function() self.constraints = self._get_constraints() def _calc_bit(self, i: int, t: int) -> int: return i + t * self.tsp_instance.number_of_cities def _get_objective_function(self) -> Polynomial: equation = Polynomial(0) for i, j in itertools.permutations( range(0, self.tsp_instance.number_of_cities), 2 ): curr = Polynomial(0) for t in range(self.tsp_instance.number_of_cities): inc_t = t + 1 if inc_t == self.tsp_instance.number_of_cities: inc_t = 0 curr += Polynomial({(f"x{self._calc_bit(i, t)}", f"x{self._calc_bit(j, inc_t)}"): 1}) equation += ( self.tsp_instance.normalized_distance_matrix[i][j] * curr ) return equation def _get_constraints(self) -> list[Constraint]: constraints: list[Constraint] = [] for i in range(self.tsp_instance.number_of_cities): equation = Polynomial(1) for t in range(self.tsp_instance.number_of_cities): equation -= Polynomial({(f"x{self._calc_bit(i, t)}",): 1}) constraints.append(Constraint(equation, group=0)) for t in range(self.tsp_instance.number_of_cities): equation = Polynomial(1) for i in range(self.tsp_instance.number_of_cities): equation -= Polynomial({(f"x{self._calc_bit(i, t)}",): 1}) constraints.append(Constraint(equation, group=1)) return constraints def _get_distance(self, order_result: np.ndarray) -> float: dist: float = 0 tab = [] for result in order_result: tab.append(list(result).index(1)) for i in range(len(tab)): j = i - 1 dist += ( self.tsp_instance.normalized_distance_matrix[tab[i]][tab[j]] ) return dist def _valid(self, order_result: np.ndarray) -> bool: return cast(bool, ( order_result.sum(0) == 1).all() and (order_result.sum(1) == 1).all() )
[docs] def get_score(self, result: np.record, penalty: float = 0) -> float: """Returns length of the route for provided numpy record. Parameters ---------- result : np.record Outcome as a numpy record with variables as keys and their values. Dtype is list of tuples with variable name and its value (0 or 1) and tuple ('probability', <float>). penalty : float, default 0 Penalty for the constraint violation Returns ------- float Returns length of the route, or 0 if route wasn't correct """ order_result = np.zeros((self.tsp_instance.number_of_cities, self.tsp_instance.number_of_cities)) for i in range(self.tsp_instance.number_of_cities): for j in range(self.tsp_instance.number_of_cities): order_result[i][j] = result[ f"x{i*self.tsp_instance.number_of_cities + j}"] if not self._valid(order_result): return penalty # Bigger value that possible distance return self._get_distance(order_result)