Add types to tsp.py

master
Alinson S. Xavier 5 years ago
parent f7545204d7
commit 38212fb858
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -1,6 +1,7 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from typing import List, Tuple, FrozenSet, Any, Optional
import networkx as nx import networkx as nx
import numpy as np import numpy as np
@ -11,16 +12,16 @@ from scipy.stats import uniform, randint
from scipy.stats.distributions import rv_frozen from scipy.stats.distributions import rv_frozen
from miplearn.instance.base import Instance from miplearn.instance.base import Instance
from miplearn.types import VariableName, Category
class ChallengeA: class ChallengeA:
def __init__( def __init__(
self, self,
seed=42, seed: int = 42,
n_training_instances=500, n_training_instances: int = 500,
n_test_instances=50, n_test_instances: int = 50,
): ) -> None:
np.random.seed(seed) np.random.seed(seed)
self.generator = TravelingSalesmanGenerator( self.generator = TravelingSalesmanGenerator(
x=uniform(loc=0.0, scale=1000.0), x=uniform(loc=0.0, scale=1000.0),
@ -38,33 +39,103 @@ class ChallengeA:
self.test_instances = self.generator.generate(n_test_instances) self.test_instances = self.generator.generate(n_test_instances)
class TravelingSalesmanInstance(Instance):
"""An instance ot the Traveling Salesman Problem.
Given a list of cities and the distance between each pair of cities, the problem
asks for the shortest route starting at the first city, visiting each other city
exactly once, then returning to the first city. This problem is a generalization
of the Hamiltonian path problem, one of Karp's 21 NP-complete problems.
"""
def __init__(self, n_cities: int, distances: np.ndarray) -> None:
super().__init__()
assert isinstance(distances, np.ndarray)
assert distances.shape == (n_cities, n_cities)
self.n_cities = n_cities
self.distances = distances
self.edges = [
(i, j) for i in range(self.n_cities) for j in range(i + 1, self.n_cities)
]
self.varname_to_index = {f"x[{e}]": e for e in self.edges}
@overrides
def to_model(self) -> pe.ConcreteModel:
model = pe.ConcreteModel()
model.x = pe.Var(self.edges, domain=pe.Binary)
model.obj = pe.Objective(
expr=sum(model.x[i, j] * self.distances[i, j] for (i, j) in self.edges),
sense=pe.minimize,
)
model.eq_degree = pe.ConstraintList()
model.eq_subtour = pe.ConstraintList()
for i in range(self.n_cities):
model.eq_degree.add(
sum(
model.x[min(i, j), max(i, j)]
for j in range(self.n_cities)
if i != j
)
== 2
)
return model
@overrides
def get_variable_category(self, var_name: VariableName) -> Category:
return self.varname_to_index[var_name]
@overrides
def find_violated_lazy_constraints(self, model: Any) -> List[FrozenSet]:
selected_edges = [e for e in self.edges if model.x[e].value > 0.5]
graph = nx.Graph()
graph.add_edges_from(selected_edges)
components = [frozenset(c) for c in list(nx.connected_components(graph))]
violations = []
for c in components:
if len(c) < self.n_cities:
violations += [c]
return violations
@overrides
def build_lazy_constraint(self, model: Any, component: FrozenSet) -> Any:
cut_edges = [
e
for e in self.edges
if (e[0] in component and e[1] not in component)
or (e[0] not in component and e[1] in component)
]
return model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2)
class TravelingSalesmanGenerator: class TravelingSalesmanGenerator:
"""Random generator for the Traveling Salesman Problem.""" """Random generator for the Traveling Salesman Problem."""
def __init__( def __init__(
self, self,
x=uniform(loc=0.0, scale=1000.0), x: rv_frozen = uniform(loc=0.0, scale=1000.0),
y=uniform(loc=0.0, scale=1000.0), y: rv_frozen = uniform(loc=0.0, scale=1000.0),
n=randint(low=100, high=101), n: rv_frozen = randint(low=100, high=101),
gamma=uniform(loc=1.0, scale=0.0), gamma: rv_frozen = uniform(loc=1.0, scale=0.0),
fix_cities=True, fix_cities: bool = True,
round=True, round: bool = True,
): ) -> None:
"""Initializes the problem generator. """Initializes the problem generator.
Initially, the generator creates n cities (x_1,y_1),...,(x_n,y_n) where n, x_i and y_i are Initially, the generator creates n cities (x_1,y_1),...,(x_n,y_n) where n,
sampled independently from the provided probability distributions `n`, `x` and `y`. For each x_i and y_i are sampled independently from the provided probability
(unordered) pair of cities (i,j), the distance d[i,j] between them is set to: distributions `n`, `x` and `y`. For each (unordered) pair of cities (i,j),
the distance d[i,j] between them is set to:
d[i,j] = gamma[i,j] \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2} d[i,j] = gamma[i,j] \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2}
where gamma is sampled from the provided probability distribution `gamma`. where gamma is sampled from the provided probability distribution `gamma`.
If fix_cities=True, the list of cities is kept the same for all generated instances. The If fix_cities=True, the list of cities is kept the same for all generated
gamma values, and therefore also the distances, are still different. instances. The gamma values, and therefore also the distances, are still
different.
By default, all distances d[i,j] are rounded to the nearest integer. If `round=False` By default, all distances d[i,j] are rounded to the nearest integer. If
is provided, this rounding will be disabled. `round=False` is provided, this rounding will be disabled.
Arguments Arguments
--------- ---------
@ -75,8 +146,8 @@ class TravelingSalesmanGenerator:
n: rv_discrete n: rv_discrete
Probability distribution for the number of cities. Probability distribution for the number of cities.
fix_cities: bool fix_cities: bool
If False, cities will be resampled for every generated instance. Otherwise, list of If False, cities will be resampled for every generated instance. Otherwise, list
cities will be computed once, during the constructor. of cities will be computed once, during the constructor.
round: bool round: bool
If True, distances are rounded to the nearest integer. If True, distances are rounded to the nearest integer.
""" """
@ -94,14 +165,17 @@ class TravelingSalesmanGenerator:
self.round = round self.round = round
if fix_cities: if fix_cities:
self.fixed_n: Optional[int]
self.fixed_cities: Optional[np.ndarray]
self.fixed_n, self.fixed_cities = self._generate_cities() self.fixed_n, self.fixed_cities = self._generate_cities()
else: else:
self.fixed_n = None self.fixed_n = None
self.fixed_cities = None self.fixed_cities = None
def generate(self, n_samples): def generate(self, n_samples: int) -> List[TravelingSalesmanInstance]:
def _sample(): def _sample() -> TravelingSalesmanInstance:
if self.fixed_cities is not None: if self.fixed_cities is not None:
assert self.fixed_n is not None
n, cities = self.fixed_n, self.fixed_cities n, cities = self.fixed_n, self.fixed_cities
else: else:
n, cities = self._generate_cities() n, cities = self._generate_cities()
@ -113,75 +187,7 @@ class TravelingSalesmanGenerator:
return [_sample() for _ in range(n_samples)] return [_sample() for _ in range(n_samples)]
def _generate_cities(self): def _generate_cities(self) -> Tuple[int, np.ndarray]:
n = self.n.rvs() n = self.n.rvs()
cities = np.array([(self.x.rvs(), self.y.rvs()) for _ in range(n)]) cities = np.array([(self.x.rvs(), self.y.rvs()) for _ in range(n)])
return n, cities return n, cities
class TravelingSalesmanInstance(Instance):
"""An instance ot the Traveling Salesman Problem.
Given a list of cities and the distance between each pair of cities, the problem asks for the
shortest route starting at the first city, visiting each other city exactly once, then
returning to the first city. This problem is a generalization of the Hamiltonian path problem,
one of Karp's 21 NP-complete problems.
"""
def __init__(self, n_cities, distances):
super().__init__()
assert isinstance(distances, np.ndarray)
assert distances.shape == (n_cities, n_cities)
self.n_cities = n_cities
self.distances = distances
self.edges = [
(i, j) for i in range(self.n_cities) for j in range(i + 1, self.n_cities)
]
self.varname_to_index = {f"x[{e}]": e for e in self.edges}
@overrides
def to_model(self):
model = pe.ConcreteModel()
model.x = pe.Var(self.edges, domain=pe.Binary)
model.obj = pe.Objective(
expr=sum(model.x[i, j] * self.distances[i, j] for (i, j) in self.edges),
sense=pe.minimize,
)
model.eq_degree = pe.ConstraintList()
model.eq_subtour = pe.ConstraintList()
for i in range(self.n_cities):
model.eq_degree.add(
sum(
model.x[min(i, j), max(i, j)]
for j in range(self.n_cities)
if i != j
)
== 2
)
return model
@overrides
def get_variable_category(self, var_name):
return self.varname_to_index[var_name]
@overrides
def find_violated_lazy_constraints(self, model):
selected_edges = [e for e in self.edges if model.x[e].value > 0.5]
graph = nx.Graph()
graph.add_edges_from(selected_edges)
components = [frozenset(c) for c in list(nx.connected_components(graph))]
violations = []
for c in components:
if len(c) < self.n_cities:
violations += [c]
return violations
@overrides
def build_lazy_constraint(self, model, component):
cut_edges = [
e
for e in self.edges
if (e[0] in component and e[1] not in component)
or (e[0] not in component and e[1] in component)
]
return model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2)

Loading…
Cancel
Save